diff --git a/.github/workflows/analyses-snapshot-lint.yaml b/.github/workflows/analyses-snapshot-lint.yaml
index 17e13e30868..7a51a5e976a 100644
--- a/.github/workflows/analyses-snapshot-lint.yaml
+++ b/.github/workflows/analyses-snapshot-lint.yaml
@@ -27,7 +27,7 @@ jobs:
- name: Setup Python
uses: 'actions/setup-python@v5'
with:
- python-version: '3.13.0-rc.3'
+ python-version: '3.13.0'
cache: 'pipenv'
cache-dependency-path: analyses-snapshot-testing/Pipfile.lock
- name: Setup
diff --git a/.github/workflows/analyses-snapshot-test.yaml b/.github/workflows/analyses-snapshot-test.yaml
index 7770db0d286..09539d873e9 100644
--- a/.github/workflows/analyses-snapshot-test.yaml
+++ b/.github/workflows/analyses-snapshot-test.yaml
@@ -78,7 +78,7 @@ jobs:
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
- python-version: '3.13.0-rc.3'
+ python-version: '3.13.0'
cache: 'pipenv'
cache-dependency-path: analyses-snapshot-testing/Pipfile.lock
diff --git a/abr-testing/protocol_simulation/__init__.py b/abr-testing/protocol_simulation/__init__.py
new file mode 100644
index 00000000000..157c21fd93e
--- /dev/null
+++ b/abr-testing/protocol_simulation/__init__.py
@@ -0,0 +1 @@
+"""The package holding code for simulating protocols."""
\ No newline at end of file
diff --git a/abr-testing/protocol_simulation/simulation_metrics.py b/abr-testing/protocol_simulation/simulation_metrics.py
new file mode 100644
index 00000000000..544bc3fb4bc
--- /dev/null
+++ b/abr-testing/protocol_simulation/simulation_metrics.py
@@ -0,0 +1,353 @@
+import re
+import sys
+import os
+from pathlib import Path
+from click import Context
+from opentrons.cli import analyze
+import json
+import argparse
+from datetime import datetime
+from abr_testing.automation import google_sheets_tool
+from abr_testing.data_collection import read_robot_logs
+from typing import Set, Dict, Any, Tuple, List, Union
+from abr_testing.tools import plate_reader
+
+def look_for_air_gaps(protocol_file_path: str) -> int:
+ instances = 0
+ try:
+ with open(protocol_file_path, "r") as open_file:
+ protocol_lines = open_file.readlines()
+ for line in protocol_lines:
+ if "air_gap" in line:
+ print(line)
+ instances += 1
+ print(f'Found {instances} instance(s) of the air gap function')
+ open_file.close()
+ except Exception as error:
+ print("Error reading protocol:", error.with_traceback())
+ return instances
+
+def set_api_level(protocol_file_path) -> None:
+ with open(protocol_file_path, "r") as file:
+ file_contents = file.readlines()
+ # Look for current'apiLevel:'
+ for i, line in enumerate(file_contents):
+ print(line)
+ if 'apiLevel' in line:
+ print(f"The current API level of this protocol is: {line}")
+ change = input("Would you like to simulate with a different API level? (Y/N) ").strip().upper()
+
+ if change == "Y":
+ api_level = input("Protocol API Level to Simulate with: ")
+ # Update new API level
+ file_contents[i] = f'apiLevel: {api_level}\n'
+ print(f"Updated line: {file_contents[i]}")
+ break
+ with open(protocol_file_path, "w") as file:
+ file.writelines(file_contents)
+ print("File updated successfully.")
+
+original_exit = sys.exit
+
+def mock_exit(code=None) -> None:
+ print(f"sys.exit() called with code: {code}")
+ raise SystemExit(code)
+
+def get_labware_name(id: str, object_dict: dict, json_data: dict) -> str:
+ slot = ""
+ for obj in object_dict:
+ if obj['id'] == id:
+ try:
+ # Try to get the slotName from the location
+ slot = obj['location']['slotName']
+ return " SLOT: " + slot
+ except KeyError:
+ location = obj.get('location', {})
+
+ # Check if location contains 'moduleId'
+ if 'moduleId' in location:
+ return get_labware_name(location['moduleId'], json_data['modules'], json_data)
+
+ # Check if location contains 'labwareId'
+ elif 'labwareId' in location:
+ return get_labware_name(location['labwareId'], json_data['labware'], json_data)
+
+ return " Labware not found"
+
+def parse_results_volume(json_data_file: str) -> Tuple[
+ List[str], List[str], List[str], List[str],
+ List[str], List[str], List[str], List[str],
+ List[str], List[str], List[str]
+ ]:
+ json_data = []
+ with open(json_data_file, "r") as json_file:
+ json_data = json.load(json_file)
+ commands = json_data.get("commands", [])
+ start_time = datetime.fromisoformat(commands[0]["createdAt"])
+ end_time = datetime.fromisoformat(commands[len(commands)-1]["completedAt"])
+ header = ["", "Protocol Name", "Date", "Time"]
+ header_fill_row = ["", protocol_name, str(file_date.date()), str(file_date.time())]
+ labware_names_row =["Labware Name"]
+ volume_dispensed_row =["Total Volume Dispensed uL"]
+ volume_aspirated_row =["Total Volume Aspirated uL"]
+ change_in_volume_row = ["Total Change in Volume uL"]
+ start_time_row = ["Start Time"]
+ end_time_row = ["End Time"]
+ total_time_row = ["Total Time of Execution"]
+ metrics_row = [
+ "Metric",
+ "Heatershaker # of Latch Open/Close",
+ "Heatershaker # of Homes",
+ "Heatershaker # of Rotations",
+ "Heatershaker Temp On Time (sec)",
+ "Temp Module # of Temp Changes",
+ "Temp Module Temp On Time (sec)",
+ "Temp Mod Time to 4C (sec)",
+ "Thermocycler # of Lid Open/Close",
+ "Thermocycler Block # of Temp Changes",
+ "Thermocycler Block Temp On Time (sec)",
+ "Thermocycler Block Time to 4C (sec)",
+ "Thermocycler Lid # of Temp Changes",
+ "Thermocycler Lid Temp On Time (sec)",
+ "Thermocycler Lid Time to 105C (sec)",
+ "Plate Reader # of Reads",
+ "Plate Reader Avg Read Time (sec)",
+ "Plate Reader # of Initializations",
+ "Plate Reader Avg Initialize Time (sec)",
+ "Plate Reader # of Lid Movements",
+ "Plate Reader Result",
+ "Left Pipette Total Tip Pick Up(s)",
+ "Left Pipette Total Aspirates",
+ "Left Pipette Total Dispenses",
+ "Right Pipette Total Tip Pick Up(s)",
+ "Right Pipette Total Aspirates",
+ "Right Pipette Total Dispenses",
+ "Gripper Pick Ups",
+ "Total Liquid Probes",
+ "Average Liquid Probe Time (sec)",
+ ]
+ values_row = ["Value"]
+ labware_well_dict = {}
+ hs_dict, temp_module_dict, thermo_cycler_dict, plate_reader_dict, instrument_dict = {}, {}, {}, {}, {}
+ try:
+ hs_dict = read_robot_logs.hs_commands(json_data)
+ temp_module_dict = read_robot_logs.temperature_module_commands(json_data)
+ thermo_cycler_dict = read_robot_logs.thermocycler_commands(json_data)
+ plate_reader_dict = read_robot_logs.plate_reader_commands(json_data, hellma_plate_standards)
+ instrument_dict = read_robot_logs.instrument_commands(json_data)
+ except:
+ pass
+
+ metrics = [hs_dict, temp_module_dict, thermo_cycler_dict, plate_reader_dict, instrument_dict]
+
+ # Iterate through all the commands executed in the protocol run log
+ for x, command in enumerate(commands):
+ if x != 0:
+ prev_command = commands[x-1]
+ if command["commandType"] == "aspirate":
+ if not (prev_command["commandType"] == "comment" and (prev_command['params']['message'] == "AIR GAP" or prev_command['params']['message'] == "MIXING")):
+ labware_id = command["params"]["labwareId"]
+ labware_name = ""
+ for labware in json_data.get("labware"):
+ if labware["id"] == labware_id:
+ labware_name = (labware["loadName"]) + get_labware_name(labware["id"], json_data["labware"], json_data)
+ well_name = command["params"]["wellName"]
+
+ if labware_id not in labware_well_dict:
+ labware_well_dict[labware_id] = {}
+
+ if well_name not in labware_well_dict[labware_id]:
+ labware_well_dict[labware_id][well_name] = (labware_name, 0, 0, "")
+
+ vol = int(command["params"]["volume"])
+
+ labware_name, added_volumes, subtracted_volumes, log = labware_well_dict[labware_id][well_name]
+
+ subtracted_volumes += vol
+ log+=(f"aspirated {vol} ")
+ labware_well_dict[labware_id][well_name] = (labware_name, added_volumes, subtracted_volumes, log)
+ elif command["commandType"] == "dispense":
+ if not (prev_command["commandType"] == "comment" and (prev_command['params']['message'] == "MIXING")):
+ labware_id = command["params"]["labwareId"]
+ labware_name = ""
+ for labware in json_data.get("labware"):
+ if labware["id"] == labware_id:
+ labware_name = (labware["loadName"]) + get_labware_name(labware["id"], json_data["labware"], json_data)
+ well_name = command["params"]["wellName"]
+
+ if labware_id not in labware_well_dict:
+ labware_well_dict[labware_id] = {}
+
+ if well_name not in labware_well_dict[labware_id]:
+ labware_well_dict[labware_id][well_name] = (labware_name, 0, 0, "")
+
+ vol = int(command["params"]["volume"])
+ labware_name, added_volumes, subtracted_volumes, log = labware_well_dict[labware_id][well_name]
+ added_volumes += vol
+ log+=(f"dispensed {vol} ")
+ labware_well_dict[labware_id][well_name] = (labware_name, added_volumes, subtracted_volumes, log)
+ # file_date_formatted = file_date.strftime("%Y-%m-%d_%H-%M-%S")
+ with open(f"{os.path.dirname(json_data_file)}\\{protocol_name}_well_volumes_{file_date_formatted}.json", "w") as output_file:
+ json.dump(labware_well_dict, output_file)
+ output_file.close()
+
+ # populate row lists
+ for labware_id in labware_well_dict.keys():
+ volume_added = 0
+ volume_subtracted = 0
+ labware_name =""
+ for well in labware_well_dict[labware_id].keys():
+ labware_name, added_volumes, subtracted_volumes, log = labware_well_dict[labware_id][well]
+ volume_added += added_volumes
+ volume_subtracted += subtracted_volumes
+ labware_names_row.append(labware_name)
+ volume_dispensed_row.append(str(volume_added))
+ volume_aspirated_row.append(str(volume_subtracted))
+ change_in_volume_row.append(str(volume_added - volume_subtracted))
+ start_time_row.append(str(start_time.time()))
+ end_time_row.append(str(end_time.time()))
+ total_time_row.append(str(end_time - start_time))
+
+ for metric in metrics:
+ for cmd in metric.keys():
+ values_row.append(str(metric[cmd]))
+ return(
+ header,
+ header_fill_row,
+ labware_names_row,
+ volume_dispensed_row,
+ volume_aspirated_row,
+ change_in_volume_row,
+ start_time_row,
+ end_time_row,
+ total_time_row,
+ metrics_row,
+ values_row)
+
+def main(storage_directory, google_sheet_name, protocol_file_path):
+ sys.exit = mock_exit
+
+ # Read file path from arguments
+ protocol_file_path = Path(protocol_file_path)
+ global protocol_name
+ protocol_name = protocol_file_path.stem
+ print("Simulating", protocol_name)
+ global file_date
+ file_date = datetime.now()
+ global file_date_formatted
+ file_date_formatted = file_date.strftime("%Y-%m-%d_%H-%M-%S")
+ # Prepare output file
+ json_file_path = f"{storage_directory}\\{protocol_name}_{file_date_formatted}.json"
+ json_file_output = open(json_file_path, "wb+")
+ error_output = f"{storage_directory}\\error_log"
+ # Run protocol simulation
+ try:
+ with Context(analyze) as ctx:
+ ctx.invoke(
+ analyze,
+ files=[protocol_file_path],
+ json_output=json_file_output,
+ human_json_output=None,
+ log_output=error_output,
+ log_level="ERROR",
+ check=False
+ )
+ except SystemExit as e:
+ print(f"SystemExit caught with code: {e}")
+ finally:
+ sys.exit = original_exit
+ json_file_output.close()
+ with open(error_output, "r") as open_file:
+ try:
+ errors = open_file.readlines()
+ if not errors: pass
+ else:
+ print(errors)
+ sys.exit(1)
+ except:
+ print("error simulating ...")
+ sys.exit()
+
+ try:
+ credentials_path = os.path.join(storage_directory, "credentials.json")
+ print(credentials_path)
+ except FileNotFoundError:
+ print(f"Add credentials.json file to: {storage_directory}.")
+ sys.exit()
+
+ global hellma_plate_standards
+
+ try:
+ hellma_plate_standards = plate_reader.read_hellma_plate_files(storage_directory, 101934)
+ except:
+ print(f"Add helma plate standard files to {storage_directory}.")
+ sys.exit()
+
+ google_sheet = google_sheets_tool.google_sheet(
+ credentials_path, google_sheet_name, 0
+ )
+
+ google_sheet.write_to_row([])
+
+ for row in parse_results_volume(json_file_path):
+ print("Writing results to", google_sheet_name)
+ print(str(row))
+ google_sheet.write_to_row(row)
+
+if __name__ == "__main__":
+ CLEAN_PROTOCOL = True
+ parser = argparse.ArgumentParser(description="Read run logs on google drive.")
+ parser.add_argument(
+ "storage_directory",
+ metavar="STORAGE_DIRECTORY",
+ type=str,
+ nargs=1,
+ help="Path to long term storage directory for run logs.",
+ )
+ parser.add_argument(
+ "sheet_name",
+ metavar="SHEETNAME",
+ type=str,
+ nargs=1,
+ help="Name of sheet to upload results to",
+ )
+ parser.add_argument(
+ "protocol_file_path",
+ metavar="PROTOCOL_FILE_PATH",
+ type=str,
+ nargs=1,
+ help="Path to protocol file"
+
+ )
+ args = parser.parse_args()
+ storage_directory = args.storage_directory[0]
+ sheet_name = args.sheet_name[0]
+ protocol_file_path = args.protocol_file_path[0]
+
+ SETUP = True
+ while(SETUP):
+ print("This current version cannot properly handle air gap calls.\nThese may cause simulation results to be inaccurate")
+ air_gaps = look_for_air_gaps(protocol_file_path)
+ if air_gaps > 0:
+ choice = ""
+ while not choice:
+ choice = input("This protocol contains air gaps, results may be innacurate, would you like to continue? (Y/N): ")
+ if choice.upper() == "Y":
+ SETUP = False
+ CLEAN_PROTOCOL = True
+ elif choice.upper() == "N":
+ CLEAN_PROTOCOL = False
+ SETUP = False
+ print("Please remove air gaps then re-run")
+ else:
+ choice = ""
+ print("Please enter a valid response.")
+ SETUP = False
+
+ if CLEAN_PROTOCOL:
+ main(
+ storage_directory,
+ sheet_name,
+ protocol_file_path,
+ )
+ else: sys.exit(0)
\ No newline at end of file
diff --git a/analyses-snapshot-testing/mypy.ini b/analyses-snapshot-testing/mypy.ini
index cab126eb42d..d5e1e97f945 100644
--- a/analyses-snapshot-testing/mypy.ini
+++ b/analyses-snapshot-testing/mypy.ini
@@ -7,7 +7,7 @@ disallow_any_generics = true
check_untyped_defs = true
no_implicit_reexport = true
exclude = "__init__.py"
-python_version = 3.12
+python_version = 3.13
plugins = pydantic.mypy
[pydantic-mypy]
diff --git a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py
index abf47212dac..ee724ea5ca3 100644
--- a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py
+++ b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py
@@ -10,16 +10,13 @@
overload,
Union,
TYPE_CHECKING,
- List,
)
from opentrons_shared_data.errors.exceptions import MotionPlanningFailureError
from opentrons_shared_data.module import FLEX_TC_LID_COLLISION_ZONE
-from opentrons.hardware_control import CriticalPoint
from opentrons.hardware_control.modules.types import ModuleType
from opentrons.motion_planning import deck_conflict as wrapped_deck_conflict
-from opentrons.motion_planning import adjacent_slots_getters
from opentrons.protocol_engine import (
StateView,
@@ -28,16 +25,10 @@
OnLabwareLocation,
AddressableAreaLocation,
OFF_DECK_LOCATION,
- WellLocation,
- DropTipWellLocation,
)
from opentrons.protocol_engine.errors.exceptions import LabwareNotLoadedOnModuleError
-from opentrons.protocol_engine.types import (
- StagingSlotLocation,
-)
from opentrons.types import DeckSlotName, StagingSlotName, Point
from ...disposal_locations import TrashBin, WasteChute
-from . import point_calculations
if TYPE_CHECKING:
from ...labware import Labware
@@ -193,294 +184,6 @@ def check(
)
-# TODO (spp, 2023-02-16): move pipette movement safety checks to its own separate file.
-def check_safe_for_pipette_movement(
- engine_state: StateView,
- pipette_id: str,
- labware_id: str,
- well_name: str,
- well_location: Union[WellLocation, DropTipWellLocation],
-) -> None:
- """Check if the labware is safe to move to with a pipette in partial tip configuration.
-
- Args:
- engine_state: engine state view
- pipette_id: ID of the pipette to be moved
- labware_id: ID of the labware we are moving to
- well_name: Name of the well to move to
- well_location: exact location within the well to move to
- """
- # TODO (spp, 2023-02-06): remove this check after thorough testing.
- # This function is capable of checking for movement conflict regardless of
- # nozzle configuration.
- if not engine_state.pipettes.get_is_partially_configured(pipette_id):
- return
-
- if isinstance(well_location, DropTipWellLocation):
- # convert to WellLocation
- well_location = engine_state.geometry.get_checked_tip_drop_location(
- pipette_id=pipette_id,
- labware_id=labware_id,
- well_location=well_location,
- partially_configured=True,
- )
- well_location_point = engine_state.geometry.get_well_position(
- labware_id=labware_id, well_name=well_name, well_location=well_location
- )
- primary_nozzle = engine_state.pipettes.get_primary_nozzle(pipette_id)
-
- destination_cp = _get_critical_point_to_use(engine_state, labware_id)
-
- pipette_bounds_at_well_location = (
- engine_state.pipettes.get_pipette_bounds_at_specified_move_to_position(
- pipette_id=pipette_id,
- destination_position=well_location_point,
- critical_point=destination_cp,
- )
- )
- if not _is_within_pipette_extents(
- engine_state=engine_state,
- pipette_id=pipette_id,
- pipette_bounding_box_at_loc=pipette_bounds_at_well_location,
- ):
- raise PartialTipMovementNotAllowedError(
- f"Requested motion with the {primary_nozzle} nozzle partial configuration"
- f" is outside of robot bounds for the pipette."
- )
-
- labware_slot = engine_state.geometry.get_ancestor_slot_name(labware_id)
-
- surrounding_slots = adjacent_slots_getters.get_surrounding_slots(
- slot=labware_slot.as_int(), robot_type=engine_state.config.robot_type
- )
-
- if _will_collide_with_thermocycler_lid(
- engine_state=engine_state,
- pipette_bounds=pipette_bounds_at_well_location,
- surrounding_regular_slots=surrounding_slots.regular_slots,
- ):
- raise PartialTipMovementNotAllowedError(
- f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot"
- f" {labware_slot} with {primary_nozzle} nozzle partial configuration"
- f" will result in collision with thermocycler lid in deck slot A1."
- )
-
- for regular_slot in surrounding_slots.regular_slots:
- if _slot_has_potential_colliding_object(
- engine_state=engine_state,
- pipette_bounds=pipette_bounds_at_well_location,
- surrounding_slot=regular_slot,
- ):
- raise PartialTipMovementNotAllowedError(
- f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot"
- f" {labware_slot} with {primary_nozzle} nozzle partial configuration"
- f" will result in collision with items in deck slot {regular_slot}."
- )
- for staging_slot in surrounding_slots.staging_slots:
- if _slot_has_potential_colliding_object(
- engine_state=engine_state,
- pipette_bounds=pipette_bounds_at_well_location,
- surrounding_slot=staging_slot,
- ):
- raise PartialTipMovementNotAllowedError(
- f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot"
- f" {labware_slot} with {primary_nozzle} nozzle partial configuration"
- f" will result in collision with items in staging slot {staging_slot}."
- )
-
-
-def _get_critical_point_to_use(
- engine_state: StateView, labware_id: str
-) -> Optional[CriticalPoint]:
- """Return the critical point to use when accessing the given labware."""
- # TODO (spp, 2024-09-17): looks like Y_CENTER of column is the same as its XY_CENTER.
- # I'm using this if-else ladder to be consistent with what we do in
- # `MotionPlanning.get_movement_waypoints_to_well()`.
- # We should probably use only XY_CENTER in both places.
- if engine_state.labware.get_should_center_column_on_target_well(labware_id):
- return CriticalPoint.Y_CENTER
- elif engine_state.labware.get_should_center_pipette_on_target_well(labware_id):
- return CriticalPoint.XY_CENTER
- return None
-
-
-def _slot_has_potential_colliding_object(
- engine_state: StateView,
- pipette_bounds: Tuple[Point, Point, Point, Point],
- surrounding_slot: Union[DeckSlotName, StagingSlotName],
-) -> bool:
- """Return the slot, if any, that has an item that the pipette might collide into."""
- # Check if slot overlaps with pipette position
- slot_pos = engine_state.addressable_areas.get_addressable_area_position(
- addressable_area_name=surrounding_slot.id,
- do_compatibility_check=False,
- )
- slot_bounds = engine_state.addressable_areas.get_addressable_area_bounding_box(
- addressable_area_name=surrounding_slot.id,
- do_compatibility_check=False,
- )
- slot_back_left_coords = Point(slot_pos.x, slot_pos.y + slot_bounds.y, slot_pos.z)
- slot_front_right_coords = Point(slot_pos.x + slot_bounds.x, slot_pos.y, slot_pos.z)
-
- # If slot overlaps with pipette bounds
- if point_calculations.are_overlapping_rectangles(
- rectangle1=(pipette_bounds[0], pipette_bounds[1]),
- rectangle2=(slot_back_left_coords, slot_front_right_coords),
- ):
- # Check z-height of items in overlapping slot
- if isinstance(surrounding_slot, DeckSlotName):
- slot_highest_z = engine_state.geometry.get_highest_z_in_slot(
- DeckSlotLocation(slotName=surrounding_slot)
- )
- else:
- slot_highest_z = engine_state.geometry.get_highest_z_in_slot(
- StagingSlotLocation(slotName=surrounding_slot)
- )
- return slot_highest_z >= pipette_bounds[0].z
- return False
-
-
-def _will_collide_with_thermocycler_lid(
- engine_state: StateView,
- pipette_bounds: Tuple[Point, Point, Point, Point],
- surrounding_regular_slots: List[DeckSlotName],
-) -> bool:
- """Return whether the pipette might collide with thermocycler's lid/clips on a Flex.
-
- If any of the pipette's bounding vertices lie inside the no-go zone of the thermocycler-
- which is the area that's to the left, back and below the thermocycler's lid's
- protruding clips, then we will mark the movement for possible collision.
-
- This could cause false raises for the case where an 8-channel is accessing the
- thermocycler labware in a location such that the pipette is in the area between
- the clips but not touching either clips. But that's a tradeoff we'll need to make
- between a complicated check involving accurate positions of all entities involved
- and a crude check that disallows all partial tip movements around the thermocycler.
- """
- # TODO (spp, 2024-02-27): Improvements:
- # - make the check dynamic according to lid state:
- # - if lid is open, check if pipette is in no-go zone
- # - if lid is closed, use the closed lid height to check for conflict
- if (
- DeckSlotName.SLOT_A1 in surrounding_regular_slots
- and engine_state.modules.is_flex_deck_with_thermocycler()
- ):
- return (
- point_calculations.are_overlapping_rectangles(
- rectangle1=(_FLEX_TC_LID_BACK_LEFT_PT, _FLEX_TC_LID_FRONT_RIGHT_PT),
- rectangle2=(pipette_bounds[0], pipette_bounds[1]),
- )
- and pipette_bounds[0].z <= _FLEX_TC_LID_BACK_LEFT_PT.z
- )
-
- return False
-
-
-def check_safe_for_tip_pickup_and_return(
- engine_state: StateView,
- pipette_id: str,
- labware_id: str,
-) -> None:
- """Check if the presence or absence of a tiprack adapter might cause any pipette movement issues.
-
- A 96 channel pipette will pick up tips using cam action when it's configured
- to use ALL nozzles. For this, the tiprack needs to be on the Flex 96 channel tiprack adapter
- or similar or the tips will not be picked up.
-
- On the other hand, if the pipette is configured with partial nozzle configuration,
- it uses the usual pipette presses to pick the tips up, in which case, having the tiprack
- on the Flex 96 channel tiprack adapter (or similar) will cause the pipette to
- crash against the adapter posts.
-
- In order to check if the 96-channel can move and pickup/drop tips safely, this method
- checks for the height attribute of the tiprack adapter rather than checking for the
- specific official adapter since users might create custom labware &/or definitions
- compatible with the official adapter.
- """
- if not engine_state.pipettes.get_channels(pipette_id) == 96:
- # Adapters only matter to 96 ch.
- return
-
- is_partial_config = engine_state.pipettes.get_is_partially_configured(pipette_id)
- tiprack_name = engine_state.labware.get_display_name(labware_id)
- tiprack_parent = engine_state.labware.get_location(labware_id)
- if isinstance(tiprack_parent, OnLabwareLocation): # tiprack is on an adapter
- is_96_ch_tiprack_adapter = engine_state.labware.get_has_quirk(
- labware_id=tiprack_parent.labwareId, quirk="tiprackAdapterFor96Channel"
- )
- tiprack_height = engine_state.labware.get_dimensions(labware_id).z
- adapter_height = engine_state.labware.get_dimensions(tiprack_parent.labwareId).z
- if is_partial_config and tiprack_height < adapter_height:
- raise PartialTipMovementNotAllowedError(
- f"{tiprack_name} cannot be on an adapter taller than the tip rack"
- f" when picking up fewer than 96 tips."
- )
- elif not is_partial_config and not is_96_ch_tiprack_adapter:
- raise UnsuitableTiprackForPipetteMotion(
- f"{tiprack_name} must be on an Opentrons Flex 96 Tip Rack Adapter"
- f" in order to pick up or return all 96 tips simultaneously."
- )
-
- elif (
- not is_partial_config
- ): # tiprack is not on adapter and pipette is in full config
- raise UnsuitableTiprackForPipetteMotion(
- f"{tiprack_name} must be on an Opentrons Flex 96 Tip Rack Adapter"
- f" in order to pick up or return all 96 tips simultaneously."
- )
-
-
-def _is_within_pipette_extents(
- engine_state: StateView,
- pipette_id: str,
- pipette_bounding_box_at_loc: Tuple[Point, Point, Point, Point],
-) -> bool:
- """Whether a given point is within the extents of a configured pipette on the specified robot."""
- channels = engine_state.pipettes.get_channels(pipette_id)
- robot_extents = engine_state.geometry.absolute_deck_extents
- (
- pip_back_left_bound,
- pip_front_right_bound,
- pip_back_right_bound,
- pip_front_left_bound,
- ) = pipette_bounding_box_at_loc
-
- # Given the padding values accounted for against the deck extents,
- # a pipette is within extents when all of the following are true:
-
- # Each corner slot full pickup case:
- # A1: Front right nozzle is within the rear and left-side padding limits
- # D1: Back right nozzle is within the front and left-side padding limits
- # A3 Front left nozzle is within the rear and right-side padding limits
- # D3: Back left nozzle is within the front and right-side padding limits
- # Thermocycler Column A2: Front right nozzle is within padding limits
-
- if channels == 96:
- return (
- pip_front_right_bound.y
- <= robot_extents.deck_extents.y + robot_extents.padding_rear
- and pip_front_right_bound.x >= robot_extents.padding_left_side
- and pip_back_right_bound.y >= robot_extents.padding_front
- and pip_back_right_bound.x >= robot_extents.padding_left_side
- and pip_front_left_bound.y
- <= robot_extents.deck_extents.y + robot_extents.padding_rear
- and pip_front_left_bound.x
- <= robot_extents.deck_extents.x + robot_extents.padding_right_side
- and pip_back_left_bound.y >= robot_extents.padding_front
- and pip_back_left_bound.x
- <= robot_extents.deck_extents.x + robot_extents.padding_right_side
- )
- # For 8ch pipettes we only check the rear and front extents
- return (
- pip_front_right_bound.y
- <= robot_extents.deck_extents.y + robot_extents.padding_rear
- and pip_back_right_bound.y >= robot_extents.padding_front
- and pip_front_left_bound.y
- <= robot_extents.deck_extents.y + robot_extents.padding_rear
- and pip_back_left_bound.y >= robot_extents.padding_front
- )
-
-
def _map_labware(
engine_state: StateView,
labware_id: str,
diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py
index 55519e7899c..8fe2b8d7f6e 100644
--- a/api/src/opentrons/protocol_api/core/engine/instrument.py
+++ b/api/src/opentrons/protocol_api/core/engine/instrument.py
@@ -34,7 +34,7 @@
from opentrons.protocol_api._nozzle_layout import NozzleLayout
from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType
from opentrons.hardware_control.nozzle_manager import NozzleMap
-from . import deck_conflict, overlap_versions
+from . import overlap_versions, pipette_movement_conflict
from ..instrument import AbstractInstrument
from .well import WellCore
@@ -153,7 +153,7 @@ def aspirate(
absolute_point=location.point,
)
)
- deck_conflict.check_safe_for_pipette_movement(
+ pipette_movement_conflict.check_safe_for_pipette_movement(
engine_state=self._engine_client.state,
pipette_id=self._pipette_id,
labware_id=labware_id,
@@ -244,7 +244,7 @@ def dispense(
absolute_point=location.point,
)
)
- deck_conflict.check_safe_for_pipette_movement(
+ pipette_movement_conflict.check_safe_for_pipette_movement(
engine_state=self._engine_client.state,
pipette_id=self._pipette_id,
labware_id=labware_id,
@@ -321,7 +321,7 @@ def blow_out(
absolute_point=location.point,
)
)
- deck_conflict.check_safe_for_pipette_movement(
+ pipette_movement_conflict.check_safe_for_pipette_movement(
engine_state=self._engine_client.state,
pipette_id=self._pipette_id,
labware_id=labware_id,
@@ -371,7 +371,7 @@ def touch_tip(
well_location = WellLocation(
origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=z_offset)
)
- deck_conflict.check_safe_for_pipette_movement(
+ pipette_movement_conflict.check_safe_for_pipette_movement(
engine_state=self._engine_client.state,
pipette_id=self._pipette_id,
labware_id=labware_id,
@@ -421,12 +421,12 @@ def pick_up_tip(
well_name=well_name,
absolute_point=location.point,
)
- deck_conflict.check_safe_for_tip_pickup_and_return(
+ pipette_movement_conflict.check_safe_for_tip_pickup_and_return(
engine_state=self._engine_client.state,
pipette_id=self._pipette_id,
labware_id=labware_id,
)
- deck_conflict.check_safe_for_pipette_movement(
+ pipette_movement_conflict.check_safe_for_pipette_movement(
engine_state=self._engine_client.state,
pipette_id=self._pipette_id,
labware_id=labware_id,
@@ -486,12 +486,12 @@ def drop_tip(
well_location = DropTipWellLocation()
if self._engine_client.state.labware.is_tiprack(labware_id):
- deck_conflict.check_safe_for_tip_pickup_and_return(
+ pipette_movement_conflict.check_safe_for_tip_pickup_and_return(
engine_state=self._engine_client.state,
pipette_id=self._pipette_id,
labware_id=labware_id,
)
- deck_conflict.check_safe_for_pipette_movement(
+ pipette_movement_conflict.check_safe_for_pipette_movement(
engine_state=self._engine_client.state,
pipette_id=self._pipette_id,
labware_id=labware_id,
diff --git a/api/src/opentrons/protocol_api/core/engine/pipette_movement_conflict.py b/api/src/opentrons/protocol_api/core/engine/pipette_movement_conflict.py
new file mode 100644
index 00000000000..bfe98e1f217
--- /dev/null
+++ b/api/src/opentrons/protocol_api/core/engine/pipette_movement_conflict.py
@@ -0,0 +1,348 @@
+"""A Protocol-Engine-friendly wrapper for opentrons.motion_planning.deck_conflict."""
+from __future__ import annotations
+import logging
+from typing import (
+ Optional,
+ Tuple,
+ Union,
+ List,
+)
+
+from opentrons_shared_data.errors.exceptions import MotionPlanningFailureError
+from opentrons_shared_data.module import FLEX_TC_LID_COLLISION_ZONE
+
+from opentrons.hardware_control import CriticalPoint
+from opentrons.motion_planning import adjacent_slots_getters
+
+from opentrons.protocol_engine import (
+ StateView,
+ DeckSlotLocation,
+ OnLabwareLocation,
+ WellLocation,
+ DropTipWellLocation,
+)
+from opentrons.protocol_engine.types import (
+ StagingSlotLocation,
+)
+from opentrons.types import DeckSlotName, StagingSlotName, Point
+from . import point_calculations
+
+
+class PartialTipMovementNotAllowedError(MotionPlanningFailureError):
+ """Error raised when trying to perform a partial tip movement to an illegal location."""
+
+ def __init__(self, message: str) -> None:
+ super().__init__(
+ message=message,
+ )
+
+
+class UnsuitableTiprackForPipetteMotion(MotionPlanningFailureError):
+ """Error raised when trying to perform a pipette movement to a tip rack, based on adapter status."""
+
+ def __init__(self, message: str) -> None:
+ super().__init__(
+ message=message,
+ )
+
+
+_log = logging.getLogger(__name__)
+
+_FLEX_TC_LID_BACK_LEFT_PT = Point(
+ x=FLEX_TC_LID_COLLISION_ZONE["back_left"]["x"],
+ y=FLEX_TC_LID_COLLISION_ZONE["back_left"]["y"],
+ z=FLEX_TC_LID_COLLISION_ZONE["back_left"]["z"],
+)
+
+_FLEX_TC_LID_FRONT_RIGHT_PT = Point(
+ x=FLEX_TC_LID_COLLISION_ZONE["front_right"]["x"],
+ y=FLEX_TC_LID_COLLISION_ZONE["front_right"]["y"],
+ z=FLEX_TC_LID_COLLISION_ZONE["front_right"]["z"],
+)
+
+
+def check_safe_for_pipette_movement(
+ engine_state: StateView,
+ pipette_id: str,
+ labware_id: str,
+ well_name: str,
+ well_location: Union[WellLocation, DropTipWellLocation],
+) -> None:
+ """Check if the labware is safe to move to with a pipette in partial tip configuration.
+
+ Args:
+ engine_state: engine state view
+ pipette_id: ID of the pipette to be moved
+ labware_id: ID of the labware we are moving to
+ well_name: Name of the well to move to
+ well_location: exact location within the well to move to
+ """
+ # TODO (spp, 2023-02-06): remove this check after thorough testing.
+ # This function is capable of checking for movement conflict regardless of
+ # nozzle configuration.
+ if not engine_state.pipettes.get_is_partially_configured(pipette_id):
+ return
+
+ if isinstance(well_location, DropTipWellLocation):
+ # convert to WellLocation
+ well_location = engine_state.geometry.get_checked_tip_drop_location(
+ pipette_id=pipette_id,
+ labware_id=labware_id,
+ well_location=well_location,
+ partially_configured=True,
+ )
+ well_location_point = engine_state.geometry.get_well_position(
+ labware_id=labware_id, well_name=well_name, well_location=well_location
+ )
+ primary_nozzle = engine_state.pipettes.get_primary_nozzle(pipette_id)
+
+ destination_cp = _get_critical_point_to_use(engine_state, labware_id)
+
+ pipette_bounds_at_well_location = (
+ engine_state.pipettes.get_pipette_bounds_at_specified_move_to_position(
+ pipette_id=pipette_id,
+ destination_position=well_location_point,
+ critical_point=destination_cp,
+ )
+ )
+ if not _is_within_pipette_extents(
+ engine_state=engine_state,
+ pipette_id=pipette_id,
+ pipette_bounding_box_at_loc=pipette_bounds_at_well_location,
+ ):
+ raise PartialTipMovementNotAllowedError(
+ f"Requested motion with the {primary_nozzle} nozzle partial configuration"
+ f" is outside of robot bounds for the pipette."
+ )
+
+ labware_slot = engine_state.geometry.get_ancestor_slot_name(labware_id)
+
+ surrounding_slots = adjacent_slots_getters.get_surrounding_slots(
+ slot=labware_slot.as_int(), robot_type=engine_state.config.robot_type
+ )
+
+ if _will_collide_with_thermocycler_lid(
+ engine_state=engine_state,
+ pipette_bounds=pipette_bounds_at_well_location,
+ surrounding_regular_slots=surrounding_slots.regular_slots,
+ ):
+ raise PartialTipMovementNotAllowedError(
+ f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot"
+ f" {labware_slot} with {primary_nozzle} nozzle partial configuration"
+ f" will result in collision with thermocycler lid in deck slot A1."
+ )
+
+ for regular_slot in surrounding_slots.regular_slots:
+ if _slot_has_potential_colliding_object(
+ engine_state=engine_state,
+ pipette_bounds=pipette_bounds_at_well_location,
+ surrounding_slot=regular_slot,
+ ):
+ raise PartialTipMovementNotAllowedError(
+ f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot"
+ f" {labware_slot} with {primary_nozzle} nozzle partial configuration"
+ f" will result in collision with items in deck slot {regular_slot}."
+ )
+ for staging_slot in surrounding_slots.staging_slots:
+ if _slot_has_potential_colliding_object(
+ engine_state=engine_state,
+ pipette_bounds=pipette_bounds_at_well_location,
+ surrounding_slot=staging_slot,
+ ):
+ raise PartialTipMovementNotAllowedError(
+ f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot"
+ f" {labware_slot} with {primary_nozzle} nozzle partial configuration"
+ f" will result in collision with items in staging slot {staging_slot}."
+ )
+
+
+def _get_critical_point_to_use(
+ engine_state: StateView, labware_id: str
+) -> Optional[CriticalPoint]:
+ """Return the critical point to use when accessing the given labware."""
+ # TODO (spp, 2024-09-17): looks like Y_CENTER of column is the same as its XY_CENTER.
+ # I'm using this if-else ladder to be consistent with what we do in
+ # `MotionPlanning.get_movement_waypoints_to_well()`.
+ # We should probably use only XY_CENTER in both places.
+ if engine_state.labware.get_should_center_column_on_target_well(labware_id):
+ return CriticalPoint.Y_CENTER
+ elif engine_state.labware.get_should_center_pipette_on_target_well(labware_id):
+ return CriticalPoint.XY_CENTER
+ return None
+
+
+def _slot_has_potential_colliding_object(
+ engine_state: StateView,
+ pipette_bounds: Tuple[Point, Point, Point, Point],
+ surrounding_slot: Union[DeckSlotName, StagingSlotName],
+) -> bool:
+ """Return the slot, if any, that has an item that the pipette might collide into."""
+ # Check if slot overlaps with pipette position
+ slot_pos = engine_state.addressable_areas.get_addressable_area_position(
+ addressable_area_name=surrounding_slot.id,
+ do_compatibility_check=False,
+ )
+ slot_bounds = engine_state.addressable_areas.get_addressable_area_bounding_box(
+ addressable_area_name=surrounding_slot.id,
+ do_compatibility_check=False,
+ )
+ slot_back_left_coords = Point(slot_pos.x, slot_pos.y + slot_bounds.y, slot_pos.z)
+ slot_front_right_coords = Point(slot_pos.x + slot_bounds.x, slot_pos.y, slot_pos.z)
+
+ # If slot overlaps with pipette bounds
+ if point_calculations.are_overlapping_rectangles(
+ rectangle1=(pipette_bounds[0], pipette_bounds[1]),
+ rectangle2=(slot_back_left_coords, slot_front_right_coords),
+ ):
+ # Check z-height of items in overlapping slot
+ if isinstance(surrounding_slot, DeckSlotName):
+ slot_highest_z = engine_state.geometry.get_highest_z_in_slot(
+ DeckSlotLocation(slotName=surrounding_slot)
+ )
+ else:
+ slot_highest_z = engine_state.geometry.get_highest_z_in_slot(
+ StagingSlotLocation(slotName=surrounding_slot)
+ )
+ return slot_highest_z >= pipette_bounds[0].z
+ return False
+
+
+def _will_collide_with_thermocycler_lid(
+ engine_state: StateView,
+ pipette_bounds: Tuple[Point, Point, Point, Point],
+ surrounding_regular_slots: List[DeckSlotName],
+) -> bool:
+ """Return whether the pipette might collide with thermocycler's lid/clips on a Flex.
+
+ If any of the pipette's bounding vertices lie inside the no-go zone of the thermocycler-
+ which is the area that's to the left, back and below the thermocycler's lid's
+ protruding clips, then we will mark the movement for possible collision.
+
+ This could cause false raises for the case where an 8-channel is accessing the
+ thermocycler labware in a location such that the pipette is in the area between
+ the clips but not touching either clips. But that's a tradeoff we'll need to make
+ between a complicated check involving accurate positions of all entities involved
+ and a crude check that disallows all partial tip movements around the thermocycler.
+ """
+ # TODO (spp, 2024-02-27): Improvements:
+ # - make the check dynamic according to lid state:
+ # - if lid is open, check if pipette is in no-go zone
+ # - if lid is closed, use the closed lid height to check for conflict
+ if (
+ DeckSlotName.SLOT_A1 in surrounding_regular_slots
+ and engine_state.modules.is_flex_deck_with_thermocycler()
+ ):
+ return (
+ point_calculations.are_overlapping_rectangles(
+ rectangle1=(_FLEX_TC_LID_BACK_LEFT_PT, _FLEX_TC_LID_FRONT_RIGHT_PT),
+ rectangle2=(pipette_bounds[0], pipette_bounds[1]),
+ )
+ and pipette_bounds[0].z <= _FLEX_TC_LID_BACK_LEFT_PT.z
+ )
+
+ return False
+
+
+def check_safe_for_tip_pickup_and_return(
+ engine_state: StateView,
+ pipette_id: str,
+ labware_id: str,
+) -> None:
+ """Check if the presence or absence of a tiprack adapter might cause any pipette movement issues.
+
+ A 96 channel pipette will pick up tips using cam action when it's configured
+ to use ALL nozzles. For this, the tiprack needs to be on the Flex 96 channel tiprack adapter
+ or similar or the tips will not be picked up.
+
+ On the other hand, if the pipette is configured with partial nozzle configuration,
+ it uses the usual pipette presses to pick the tips up, in which case, having the tiprack
+ on the Flex 96 channel tiprack adapter (or similar) will cause the pipette to
+ crash against the adapter posts.
+
+ In order to check if the 96-channel can move and pickup/drop tips safely, this method
+ checks for the height attribute of the tiprack adapter rather than checking for the
+ specific official adapter since users might create custom labware &/or definitions
+ compatible with the official adapter.
+ """
+ if not engine_state.pipettes.get_channels(pipette_id) == 96:
+ # Adapters only matter to 96 ch.
+ return
+
+ is_partial_config = engine_state.pipettes.get_is_partially_configured(pipette_id)
+ tiprack_name = engine_state.labware.get_display_name(labware_id)
+ tiprack_parent = engine_state.labware.get_location(labware_id)
+ if isinstance(tiprack_parent, OnLabwareLocation): # tiprack is on an adapter
+ is_96_ch_tiprack_adapter = engine_state.labware.get_has_quirk(
+ labware_id=tiprack_parent.labwareId, quirk="tiprackAdapterFor96Channel"
+ )
+ tiprack_height = engine_state.labware.get_dimensions(labware_id).z
+ adapter_height = engine_state.labware.get_dimensions(tiprack_parent.labwareId).z
+ if is_partial_config and tiprack_height < adapter_height:
+ raise PartialTipMovementNotAllowedError(
+ f"{tiprack_name} cannot be on an adapter taller than the tip rack"
+ f" when picking up fewer than 96 tips."
+ )
+ elif not is_partial_config and not is_96_ch_tiprack_adapter:
+ raise UnsuitableTiprackForPipetteMotion(
+ f"{tiprack_name} must be on an Opentrons Flex 96 Tip Rack Adapter"
+ f" in order to pick up or return all 96 tips simultaneously."
+ )
+
+ elif (
+ not is_partial_config
+ ): # tiprack is not on adapter and pipette is in full config
+ raise UnsuitableTiprackForPipetteMotion(
+ f"{tiprack_name} must be on an Opentrons Flex 96 Tip Rack Adapter"
+ f" in order to pick up or return all 96 tips simultaneously."
+ )
+
+
+def _is_within_pipette_extents(
+ engine_state: StateView,
+ pipette_id: str,
+ pipette_bounding_box_at_loc: Tuple[Point, Point, Point, Point],
+) -> bool:
+ """Whether a given point is within the extents of a configured pipette on the specified robot."""
+ channels = engine_state.pipettes.get_channels(pipette_id)
+ robot_extents = engine_state.geometry.absolute_deck_extents
+ (
+ pip_back_left_bound,
+ pip_front_right_bound,
+ pip_back_right_bound,
+ pip_front_left_bound,
+ ) = pipette_bounding_box_at_loc
+
+ # Given the padding values accounted for against the deck extents,
+ # a pipette is within extents when all of the following are true:
+
+ # Each corner slot full pickup case:
+ # A1: Front right nozzle is within the rear and left-side padding limits
+ # D1: Back right nozzle is within the front and left-side padding limits
+ # A3 Front left nozzle is within the rear and right-side padding limits
+ # D3: Back left nozzle is within the front and right-side padding limits
+ # Thermocycler Column A2: Front right nozzle is within padding limits
+
+ if channels == 96:
+ return (
+ pip_front_right_bound.y
+ <= robot_extents.deck_extents.y + robot_extents.padding_rear
+ and pip_front_right_bound.x >= robot_extents.padding_left_side
+ and pip_back_right_bound.y >= robot_extents.padding_front
+ and pip_back_right_bound.x >= robot_extents.padding_left_side
+ and pip_front_left_bound.y
+ <= robot_extents.deck_extents.y + robot_extents.padding_rear
+ and pip_front_left_bound.x
+ <= robot_extents.deck_extents.x + robot_extents.padding_right_side
+ and pip_back_left_bound.y >= robot_extents.padding_front
+ and pip_back_left_bound.x
+ <= robot_extents.deck_extents.x + robot_extents.padding_right_side
+ )
+ # For 8ch pipettes we only check the rear and front extents
+ return (
+ pip_front_right_bound.y
+ <= robot_extents.deck_extents.y + robot_extents.padding_rear
+ and pip_back_right_bound.y >= robot_extents.padding_front
+ and pip_front_left_bound.y
+ <= robot_extents.deck_extents.y + robot_extents.padding_rear
+ and pip_back_left_bound.y >= robot_extents.padding_front
+ )
diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py
index 27e417aa8b4..4f132ac3b40 100644
--- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py
+++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py
@@ -1,19 +1,20 @@
"""Helper functions for liquid-level related calculations inside a given frustum."""
-from typing import List, Tuple, Iterator, Sequence, Any, Union, Optional
+from typing import List, Tuple
from numpy import pi, iscomplex, roots, real
from math import isclose
-from ..errors.exceptions import InvalidLiquidHeightFound, InvalidWellDefinitionError
-from opentrons_shared_data.labware.types import (
- is_circular_frusta_list,
- is_rectangular_frusta_list,
- CircularBoundedSection,
- RectangularBoundedSection,
+from ..errors.exceptions import InvalidLiquidHeightFound
+
+from opentrons_shared_data.labware.labware_definition import (
+ InnerWellGeometry,
+ WellSegment,
+ SphericalSegment,
+ ConicalFrustum,
+ CuboidalFrustum,
)
-from opentrons_shared_data.labware.labware_definition import InnerWellGeometry
-def reject_unacceptable_heights(
+def _reject_unacceptable_heights(
potential_heights: List[float], max_height: float
) -> float:
"""Reject any solutions to a polynomial equation that cannot be the height of a frustum."""
@@ -33,34 +34,18 @@ def reject_unacceptable_heights(
return valid_heights[0]
-def get_cross_section_area(
- bounded_section: Union[CircularBoundedSection, RectangularBoundedSection]
-) -> float:
- """Find the shape of a cross-section and calculate the area appropriately."""
- if bounded_section["shape"] == "circular":
- cross_section_area = cross_section_area_circular(bounded_section["diameter"])
- elif bounded_section["shape"] == "rectangular":
- cross_section_area = cross_section_area_rectangular(
- bounded_section["xDimension"],
- bounded_section["yDimension"],
- )
- else:
- raise InvalidWellDefinitionError(message="Invalid well volume components.")
- return cross_section_area
-
-
-def cross_section_area_circular(diameter: float) -> float:
+def _cross_section_area_circular(diameter: float) -> float:
"""Get the area of a circular cross-section."""
radius = diameter / 2
return pi * (radius**2)
-def cross_section_area_rectangular(x_dimension: float, y_dimension: float) -> float:
+def _cross_section_area_rectangular(x_dimension: float, y_dimension: float) -> float:
"""Get the area of a rectangular cross-section."""
return x_dimension * y_dimension
-def rectangular_frustum_polynomial_roots(
+def _rectangular_frustum_polynomial_roots(
bottom_length: float,
bottom_width: float,
top_length: float,
@@ -82,7 +67,7 @@ def rectangular_frustum_polynomial_roots(
return a, b, c
-def circular_frustum_polynomial_roots(
+def _circular_frustum_polynomial_roots(
bottom_radius: float,
top_radius: float,
total_frustum_height: float,
@@ -95,14 +80,14 @@ def circular_frustum_polynomial_roots(
return a, b, c
-def volume_from_height_circular(
+def _volume_from_height_circular(
target_height: float,
total_frustum_height: float,
bottom_radius: float,
top_radius: float,
) -> float:
"""Find the volume given a height within a circular frustum."""
- a, b, c = circular_frustum_polynomial_roots(
+ a, b, c = _circular_frustum_polynomial_roots(
bottom_radius=bottom_radius,
top_radius=top_radius,
total_frustum_height=total_frustum_height,
@@ -111,7 +96,7 @@ def volume_from_height_circular(
return volume
-def volume_from_height_rectangular(
+def _volume_from_height_rectangular(
target_height: float,
total_frustum_height: float,
bottom_length: float,
@@ -120,7 +105,7 @@ def volume_from_height_rectangular(
top_width: float,
) -> float:
"""Find the volume given a height within a rectangular frustum."""
- a, b, c = rectangular_frustum_polynomial_roots(
+ a, b, c = _rectangular_frustum_polynomial_roots(
bottom_length=bottom_length,
bottom_width=bottom_width,
top_length=top_length,
@@ -131,7 +116,7 @@ def volume_from_height_rectangular(
return volume
-def volume_from_height_spherical(
+def _volume_from_height_spherical(
target_height: float,
radius_of_curvature: float,
) -> float:
@@ -142,14 +127,14 @@ def volume_from_height_spherical(
return volume
-def height_from_volume_circular(
+def _height_from_volume_circular(
volume: float,
total_frustum_height: float,
bottom_radius: float,
top_radius: float,
) -> float:
"""Find the height given a volume within a circular frustum."""
- a, b, c = circular_frustum_polynomial_roots(
+ a, b, c = _circular_frustum_polynomial_roots(
bottom_radius=bottom_radius,
top_radius=top_radius,
total_frustum_height=total_frustum_height,
@@ -158,14 +143,14 @@ def height_from_volume_circular(
x_intercept_roots = (a, b, c, d)
height_from_volume_roots = roots(x_intercept_roots)
- height = reject_unacceptable_heights(
+ height = _reject_unacceptable_heights(
potential_heights=list(height_from_volume_roots),
max_height=total_frustum_height,
)
return height
-def height_from_volume_rectangular(
+def _height_from_volume_rectangular(
volume: float,
total_frustum_height: float,
bottom_length: float,
@@ -174,7 +159,7 @@ def height_from_volume_rectangular(
top_width: float,
) -> float:
"""Find the height given a volume within a rectangular frustum."""
- a, b, c = rectangular_frustum_polynomial_roots(
+ a, b, c = _rectangular_frustum_polynomial_roots(
bottom_length=bottom_length,
bottom_width=bottom_width,
top_length=top_length,
@@ -185,14 +170,14 @@ def height_from_volume_rectangular(
x_intercept_roots = (a, b, c, d)
height_from_volume_roots = roots(x_intercept_roots)
- height = reject_unacceptable_heights(
+ height = _reject_unacceptable_heights(
potential_heights=list(height_from_volume_roots),
max_height=total_frustum_height,
)
return height
-def height_from_volume_spherical(
+def _height_from_volume_spherical(
volume: float,
radius_of_curvature: float,
total_frustum_height: float,
@@ -205,20 +190,43 @@ def height_from_volume_spherical(
x_intercept_roots = (a, b, c, d)
height_from_volume_roots = roots(x_intercept_roots)
- height = reject_unacceptable_heights(
+ height = _reject_unacceptable_heights(
potential_heights=list(height_from_volume_roots),
max_height=total_frustum_height,
)
return height
-def get_boundary_pairs(frusta: Sequence[Any]) -> Iterator[Tuple[Any, Any]]:
- """Yield tuples representing two cross-section boundaries of a segment of a well."""
- iter_f = iter(frusta)
- el = next(iter_f)
- for next_el in iter_f:
- yield el, next_el
- el = next_el
+def _get_segment_capacity(segment: WellSegment) -> float:
+ match segment:
+ case SphericalSegment():
+ return _volume_from_height_spherical(
+ target_height=segment.topHeight,
+ radius_of_curvature=segment.radiusOfCurvature,
+ )
+ case CuboidalFrustum():
+ section_height = segment.topHeight - segment.bottomHeight
+ return _volume_from_height_rectangular(
+ target_height=section_height,
+ bottom_length=segment.bottomYDimension,
+ bottom_width=segment.bottomXDimension,
+ top_length=segment.topYDimension,
+ top_width=segment.topXDimension,
+ total_frustum_height=section_height,
+ )
+ case ConicalFrustum():
+ section_height = segment.topHeight - segment.bottomHeight
+ return _volume_from_height_circular(
+ target_height=section_height,
+ total_frustum_height=section_height,
+ bottom_radius=(segment.bottomDiameter / 2),
+ top_radius=(segment.topDiameter / 2),
+ )
+ case _:
+ # TODO: implement volume calculations for truncated circular and rounded rectangular segments
+ raise NotImplementedError(
+ f"volume calculation for shape: {segment.shape} not yet implemented."
+ )
def get_well_volumetric_capacity(
@@ -228,140 +236,105 @@ def get_well_volumetric_capacity(
# dictionary map of heights to volumetric capacities within their respective segment
# {top_height_0: volume_0, top_height_1: volume_1, top_height_2: volume_2}
well_volume = []
- if well_geometry.bottomShape is not None:
- if well_geometry.bottomShape.shape == "spherical":
- bottom_spherical_section_depth = well_geometry.bottomShape.depth
- bottom_sphere_volume = volume_from_height_spherical(
- radius_of_curvature=well_geometry.bottomShape.radiusOfCurvature,
- target_height=bottom_spherical_section_depth,
- )
- well_volume.append((bottom_spherical_section_depth, bottom_sphere_volume))
-
- # get the volume of remaining frusta sorted in ascending order
- sorted_frusta = sorted(well_geometry.frusta, key=lambda section: section.topHeight)
-
- if is_rectangular_frusta_list(sorted_frusta):
- for f, next_f in get_boundary_pairs(sorted_frusta):
- top_cross_section_width = next_f["xDimension"]
- top_cross_section_length = next_f["yDimension"]
- bottom_cross_section_width = f["xDimension"]
- bottom_cross_section_length = f["yDimension"]
- frustum_height = next_f["topHeight"] - f["topHeight"]
- frustum_volume = volume_from_height_rectangular(
- target_height=frustum_height,
- total_frustum_height=frustum_height,
- bottom_length=bottom_cross_section_length,
- bottom_width=bottom_cross_section_width,
- top_length=top_cross_section_length,
- top_width=top_cross_section_width,
- )
- well_volume.append((next_f["topHeight"], frustum_volume))
- elif is_circular_frusta_list(sorted_frusta):
- for f, next_f in get_boundary_pairs(sorted_frusta):
- top_cross_section_radius = next_f["diameter"] / 2.0
- bottom_cross_section_radius = f["diameter"] / 2.0
- frustum_height = next_f["topHeight"] - f["topHeight"]
- frustum_volume = volume_from_height_circular(
- target_height=frustum_height,
- total_frustum_height=frustum_height,
- bottom_radius=bottom_cross_section_radius,
- top_radius=top_cross_section_radius,
- )
+ # get the well segments sorted in ascending order
+ sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight)
- well_volume.append((next_f["topHeight"], frustum_volume))
- else:
- raise NotImplementedError(
- "Well section with differing boundary shapes not yet implemented."
- )
+ for segment in sorted_well:
+ section_volume = _get_segment_capacity(segment)
+ well_volume.append((segment.topHeight, section_volume))
return well_volume
def height_at_volume_within_section(
- top_cross_section: Union[CircularBoundedSection, RectangularBoundedSection],
- bottom_cross_section: Union[CircularBoundedSection, RectangularBoundedSection],
+ section: WellSegment,
target_volume_relative: float,
- frustum_height: float,
+ section_height: float,
) -> float:
"""Calculate a height within a bounded section according to geometry."""
- if top_cross_section["shape"] == bottom_cross_section["shape"] == "circular":
- frustum_height = height_from_volume_circular(
- volume=target_volume_relative,
- top_radius=(top_cross_section["diameter"] / 2),
- bottom_radius=(bottom_cross_section["diameter"] / 2),
- total_frustum_height=frustum_height,
- )
- elif top_cross_section["shape"] == bottom_cross_section["shape"] == "rectangular":
- frustum_height = height_from_volume_rectangular(
- volume=target_volume_relative,
- total_frustum_height=frustum_height,
- bottom_width=bottom_cross_section["xDimension"],
- bottom_length=bottom_cross_section["yDimension"],
- top_width=top_cross_section["xDimension"],
- top_length=top_cross_section["yDimension"],
- )
- else:
- raise NotImplementedError(
- "Height from volume calculation not yet implemented for this well shape."
- )
- return frustum_height
+ match section:
+ case SphericalSegment():
+ return _height_from_volume_spherical(
+ volume=target_volume_relative,
+ total_frustum_height=section_height,
+ radius_of_curvature=section.radiusOfCurvature,
+ )
+ case ConicalFrustum():
+ return _height_from_volume_circular(
+ volume=target_volume_relative,
+ top_radius=(section.bottomDiameter / 2),
+ bottom_radius=(section.topDiameter / 2),
+ total_frustum_height=section_height,
+ )
+ case CuboidalFrustum():
+ return _height_from_volume_rectangular(
+ volume=target_volume_relative,
+ total_frustum_height=section_height,
+ bottom_width=section.bottomXDimension,
+ bottom_length=section.bottomYDimension,
+ top_width=section.topXDimension,
+ top_length=section.topYDimension,
+ )
+ case _:
+ raise NotImplementedError(
+ "Height from volume calculation not yet implemented for this well shape."
+ )
def volume_at_height_within_section(
- top_cross_section: Union[CircularBoundedSection, RectangularBoundedSection],
- bottom_cross_section: Union[CircularBoundedSection, RectangularBoundedSection],
+ section: WellSegment,
target_height_relative: float,
- frustum_height: float,
+ section_height: float,
) -> float:
"""Calculate a volume within a bounded section according to geometry."""
- if top_cross_section["shape"] == bottom_cross_section["shape"] == "circular":
- frustum_volume = volume_from_height_circular(
- target_height=target_height_relative,
- total_frustum_height=frustum_height,
- bottom_radius=(bottom_cross_section["diameter"] / 2),
- top_radius=(top_cross_section["diameter"] / 2),
- )
- elif top_cross_section["shape"] == bottom_cross_section["shape"] == "rectangular":
- frustum_volume = volume_from_height_rectangular(
- target_height=target_height_relative,
- total_frustum_height=frustum_height,
- bottom_width=bottom_cross_section["xDimension"],
- bottom_length=bottom_cross_section["yDimension"],
- top_width=top_cross_section["xDimension"],
- top_length=top_cross_section["yDimension"],
- )
- # TODO(cm): this would be the NEST-96 2uL wells referenced in EXEC-712
- # we need to input the math attached to that issue
- else:
- raise NotImplementedError(
- "Height from volume calculation not yet implemented for this well shape."
- )
- return frustum_volume
+ match section:
+ case SphericalSegment():
+ return _volume_from_height_spherical(
+ target_height=target_height_relative,
+ radius_of_curvature=section.radiusOfCurvature,
+ )
+ case ConicalFrustum():
+ return _volume_from_height_circular(
+ target_height=target_height_relative,
+ total_frustum_height=section_height,
+ bottom_radius=(section.bottomDiameter / 2),
+ top_radius=(section.topDiameter / 2),
+ )
+ case CuboidalFrustum():
+ return _volume_from_height_rectangular(
+ target_height=target_height_relative,
+ total_frustum_height=section_height,
+ bottom_width=section.bottomXDimension,
+ bottom_length=section.bottomYDimension,
+ top_width=section.topXDimension,
+ top_length=section.topYDimension,
+ )
+ case _:
+ # TODO(cm): this would be the NEST-96 2uL wells referenced in EXEC-712
+ # we need to input the math attached to that issue
+ raise NotImplementedError(
+ "Height from volume calculation not yet implemented for this well shape."
+ )
def _find_volume_in_partial_frustum(
- sorted_frusta: List[Any],
+ sorted_well: List[WellSegment],
target_height: float,
-) -> Optional[float]:
+) -> float:
"""Look through a sorted list of frusta for a target height, and find the volume at that height."""
- partial_volume: Optional[float] = None
- for bottom_cross_section, top_cross_section in get_boundary_pairs(sorted_frusta):
- if (
- bottom_cross_section["topHeight"]
- < target_height
- < top_cross_section["targetHeight"]
- ):
- relative_target_height = target_height - bottom_cross_section["topHeight"]
- frustum_height = (
- top_cross_section["topHeight"] - bottom_cross_section["topHeight"]
- )
- partial_volume = volume_at_height_within_section(
- top_cross_section=top_cross_section,
- bottom_cross_section=bottom_cross_section,
+ for segment in sorted_well:
+ if segment.bottomHeight < target_height < segment.topHeight:
+ relative_target_height = target_height - segment.bottomHeight
+ section_height = segment.topHeight - segment.bottomHeight
+ return volume_at_height_within_section(
+ section=segment,
target_height_relative=relative_target_height,
- frustum_height=frustum_height,
+ section_height=section_height,
)
- return partial_volume
+ # if we've looked through all sections and can't find the target volume, raise an error
+ raise InvalidLiquidHeightFound(
+ f"Unable to find volume at given well-height {target_height}."
+ )
def find_volume_at_well_height(
@@ -384,53 +357,41 @@ def find_volume_at_well_height(
if target_height == boundary_height:
return closed_section_volume
# find the section the target height is in and compute the volume
- # since bottomShape is not in list of frusta, check here first
- if well_geometry.bottomShape:
- bottom_segment_height = volumetric_capacity[0][0]
- if (
- target_height < bottom_segment_height
- and well_geometry.bottomShape.shape == "spherical"
- ):
- return volume_from_height_spherical(
- target_height=target_height,
- radius_of_curvature=well_geometry.bottomShape.radiusOfCurvature,
- )
- sorted_frusta = sorted(well_geometry.frusta, key=lambda section: section.topHeight)
- # TODO(cm): handle non-frustum section that is not at the bottom.
+
+ sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight)
partial_volume = _find_volume_in_partial_frustum(
- sorted_frusta=sorted_frusta,
+ sorted_well=sorted_well,
target_height=target_height,
)
- if not partial_volume:
- raise InvalidLiquidHeightFound("Unable to find volume at given well-height.")
return partial_volume + closed_section_volume
def _find_height_in_partial_frustum(
- sorted_frusta: List[Any],
+ sorted_well: List[WellSegment],
volumetric_capacity: List[Tuple[float, float]],
target_volume: float,
-) -> Optional[float]:
+) -> float:
"""Look through a sorted list of frusta for a target volume, and find the height at that volume."""
- well_height: Optional[float] = None
- for cross_sections, capacity in zip(
- get_boundary_pairs(sorted_frusta),
- get_boundary_pairs(volumetric_capacity),
- ):
- bottom_cross_section, top_cross_section = cross_sections
- (bottom_height, bottom_volume), (top_height, top_volume) = capacity
-
- if bottom_volume < target_volume < top_volume:
- relative_target_volume = target_volume - bottom_volume
- frustum_height = top_height - bottom_height
+ bottom_section_volume = 0.0
+ for section, capacity in zip(sorted_well, volumetric_capacity):
+ section_top_height, section_volume = capacity
+ if bottom_section_volume < target_volume < section_volume:
+ relative_target_volume = target_volume - bottom_section_volume
+ relative_section_height = section.topHeight - section.bottomHeight
partial_height = height_at_volume_within_section(
- top_cross_section=top_cross_section,
- bottom_cross_section=bottom_cross_section,
+ section=section,
target_volume_relative=relative_target_volume,
- frustum_height=frustum_height,
+ section_height=relative_section_height,
)
- well_height = partial_height + bottom_height
- return well_height
+ return partial_height + section.bottomHeight
+ # bottom section volume should always be the volume enclosed in the previously
+ # viewed section
+ bottom_section_volume = section_volume
+
+ # if we've looked through all sections and can't find the target volume, raise an error
+ raise InvalidLiquidHeightFound(
+ f"Unable to find height at given volume {target_volume}."
+ )
def find_height_at_well_volume(
@@ -442,29 +403,10 @@ def find_height_at_well_volume(
if target_volume < 0 or target_volume > max_volume:
raise InvalidLiquidHeightFound("Invalid target volume.")
- sorted_frusta = sorted(well_geometry.frusta, key=lambda section: section.topHeight)
+ sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight)
# find the section the target volume is in and compute the height
- # since bottomShape is not in list of frusta, check here first
- if well_geometry.bottomShape:
- volume_within_bottom_segment = volumetric_capacity[0][1]
- if (
- target_volume < volume_within_bottom_segment
- and well_geometry.bottomShape.shape == "spherical"
- ):
- return height_from_volume_spherical(
- volume=target_volume,
- radius_of_curvature=well_geometry.bottomShape.radiusOfCurvature,
- total_frustum_height=well_geometry.bottomShape.depth,
- )
- # if bottom shape is present but doesn't contain the target volume,
- # then we need to look through the volumetric capacity list without the bottom shape
- # so volumetric_capacity and sorted_frusta will be aligned
- volumetric_capacity.pop(0)
- well_height = _find_height_in_partial_frustum(
- sorted_frusta=sorted_frusta,
+ return _find_height_in_partial_frustum(
+ sorted_well=sorted_well,
volumetric_capacity=volumetric_capacity,
target_volume=target_volume,
)
- if not well_height:
- raise InvalidLiquidHeightFound("Unable to find height at given well-volume.")
- return well_height
diff --git a/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py b/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py
index 9a46318c8b8..42e17983018 100644
--- a/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py
+++ b/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py
@@ -19,7 +19,7 @@
_TRASH_BIN_CUTOUT_FIXTURE,
)
from opentrons.protocol_api.labware import Labware
-from opentrons.protocol_api.core.engine import deck_conflict
+from opentrons.protocol_api.core.engine import deck_conflict, pipette_movement_conflict
from opentrons.protocol_engine import (
Config,
DeckSlotLocation,
@@ -441,7 +441,7 @@ def test_maps_trash_bins(
Point(x=50, y=50, z=40),
),
pytest.raises(
- deck_conflict.PartialTipMovementNotAllowedError,
+ pipette_movement_conflict.PartialTipMovementNotAllowedError,
match="collision with items in deck slot D1",
),
0,
@@ -454,7 +454,7 @@ def test_maps_trash_bins(
Point(x=101, y=50, z=40),
),
pytest.raises(
- deck_conflict.PartialTipMovementNotAllowedError,
+ pipette_movement_conflict.PartialTipMovementNotAllowedError,
match="collision with items in deck slot D2",
),
0,
@@ -467,7 +467,7 @@ def test_maps_trash_bins(
Point(x=250, y=150, z=40),
),
pytest.raises(
- deck_conflict.PartialTipMovementNotAllowedError,
+ pipette_movement_conflict.PartialTipMovementNotAllowedError,
match="will result in collision with items in staging slot C4.",
),
170,
@@ -623,7 +623,7 @@ def test_deck_conflict_raises_for_bad_pipette_move(
).then_return(Dimensions(90, 90, 0))
with expected_raise:
- deck_conflict.check_safe_for_pipette_movement(
+ pipette_movement_conflict.check_safe_for_pipette_movement(
engine_state=mock_state_view,
pipette_id="pipette-id",
labware_id="destination-labware-id",
@@ -726,10 +726,10 @@ def test_deck_conflict_raises_for_collision_with_tc_lid(
True
)
with pytest.raises(
- deck_conflict.PartialTipMovementNotAllowedError,
+ pipette_movement_conflict.PartialTipMovementNotAllowedError,
match="Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.",
):
- deck_conflict.check_safe_for_pipette_movement(
+ pipette_movement_conflict.check_safe_for_pipette_movement(
engine_state=mock_state_view,
pipette_id="pipette-id",
labware_id="destination-labware-id",
@@ -829,7 +829,7 @@ class PipetteMovementSpec(NamedTuple):
is_on_flex_adapter=False,
is_partial_config=False,
expected_raise=pytest.raises(
- deck_conflict.UnsuitableTiprackForPipetteMotion,
+ pipette_movement_conflict.UnsuitableTiprackForPipetteMotion,
match="A cool tiprack must be on an Opentrons Flex 96 Tip Rack Adapter",
),
),
@@ -846,7 +846,7 @@ class PipetteMovementSpec(NamedTuple):
is_on_flex_adapter=False,
is_partial_config=False,
expected_raise=pytest.raises(
- deck_conflict.UnsuitableTiprackForPipetteMotion,
+ pipette_movement_conflict.UnsuitableTiprackForPipetteMotion,
match="A cool tiprack must be on an Opentrons Flex 96 Tip Rack Adapter",
),
),
@@ -856,7 +856,7 @@ class PipetteMovementSpec(NamedTuple):
is_on_flex_adapter=True,
is_partial_config=True,
expected_raise=pytest.raises(
- deck_conflict.PartialTipMovementNotAllowedError,
+ pipette_movement_conflict.PartialTipMovementNotAllowedError,
match="A cool tiprack cannot be on an adapter taller than the tip rack",
),
),
@@ -918,7 +918,7 @@ def test_valid_96_pipette_movement_for_tiprack_and_adapter(
).then_return(is_on_flex_adapter)
with expected_raise:
- deck_conflict.check_safe_for_tip_pickup_and_return(
+ pipette_movement_conflict.check_safe_for_tip_pickup_and_return(
engine_state=mock_state_view,
pipette_id="pipette-id",
labware_id="labware-id",
diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py
index 8854c070ef0..bd3cebe94d7 100644
--- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py
+++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py
@@ -44,7 +44,7 @@
InstrumentCore,
WellCore,
ProtocolCore,
- deck_conflict,
+ pipette_movement_conflict,
)
from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION
from opentrons.protocols.api_support.types import APIVersion
@@ -76,8 +76,10 @@ def patch_mock_pipette_movement_safety_check(
decoy: Decoy, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Replace deck_conflict.check() with a mock."""
- mock = decoy.mock(func=deck_conflict.check_safe_for_pipette_movement)
- monkeypatch.setattr(deck_conflict, "check_safe_for_pipette_movement", mock)
+ mock = decoy.mock(func=pipette_movement_conflict.check_safe_for_pipette_movement)
+ monkeypatch.setattr(
+ pipette_movement_conflict, "check_safe_for_pipette_movement", mock
+ )
@pytest.fixture
@@ -271,12 +273,12 @@ def test_pick_up_tip(
)
decoy.verify(
- deck_conflict.check_safe_for_tip_pickup_and_return(
+ pipette_movement_conflict.check_safe_for_tip_pickup_and_return(
engine_state=mock_engine_client.state,
pipette_id="abc123",
labware_id="labware-id",
),
- deck_conflict.check_safe_for_pipette_movement(
+ pipette_movement_conflict.check_safe_for_pipette_movement(
engine_state=mock_engine_client.state,
pipette_id="abc123",
labware_id="labware-id",
@@ -325,7 +327,7 @@ def test_drop_tip_no_location(
subject.drop_tip(location=None, well_core=well_core, home_after=True)
decoy.verify(
- deck_conflict.check_safe_for_pipette_movement(
+ pipette_movement_conflict.check_safe_for_pipette_movement(
engine_state=mock_engine_client.state,
pipette_id="abc123",
labware_id="labware-id",
@@ -376,12 +378,12 @@ def test_drop_tip_with_location(
subject.drop_tip(location=location, well_core=well_core, home_after=True)
decoy.verify(
- deck_conflict.check_safe_for_tip_pickup_and_return(
+ pipette_movement_conflict.check_safe_for_tip_pickup_and_return(
engine_state=mock_engine_client.state,
pipette_id="abc123",
labware_id="labware-id",
),
- deck_conflict.check_safe_for_pipette_movement(
+ pipette_movement_conflict.check_safe_for_pipette_movement(
engine_state=mock_engine_client.state,
pipette_id="abc123",
labware_id="labware-id",
@@ -504,7 +506,7 @@ def test_aspirate_from_well(
)
decoy.verify(
- deck_conflict.check_safe_for_pipette_movement(
+ pipette_movement_conflict.check_safe_for_pipette_movement(
engine_state=mock_engine_client.state,
pipette_id="abc123",
labware_id="123abc",
@@ -618,7 +620,7 @@ def test_blow_out_to_well(
subject.blow_out(location=location, well_core=well_core, in_place=False)
decoy.verify(
- deck_conflict.check_safe_for_pipette_movement(
+ pipette_movement_conflict.check_safe_for_pipette_movement(
engine_state=mock_engine_client.state,
pipette_id="abc123",
labware_id="123abc",
@@ -729,7 +731,7 @@ def test_dispense_to_well(
)
decoy.verify(
- deck_conflict.check_safe_for_pipette_movement(
+ pipette_movement_conflict.check_safe_for_pipette_movement(
engine_state=mock_engine_client.state,
pipette_id="abc123",
labware_id="123abc",
@@ -1113,7 +1115,7 @@ def test_touch_tip(
)
decoy.verify(
- deck_conflict.check_safe_for_pipette_movement(
+ pipette_movement_conflict.check_safe_for_pipette_movement(
engine_state=mock_engine_client.state,
pipette_id="abc123",
labware_id="123abc",
diff --git a/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py b/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py
index ebaf5e49971..cad2bffddf9 100644
--- a/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py
+++ b/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py
@@ -4,7 +4,7 @@
from opentrons import simulate
from opentrons.protocol_api import COLUMN, ALL, SINGLE, ROW
-from opentrons.protocol_api.core.engine.deck_conflict import (
+from opentrons.protocol_api.core.engine.pipette_movement_conflict import (
PartialTipMovementNotAllowedError,
)
diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py
index 6bbd13c5e25..427dececa7b 100644
--- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py
+++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py
@@ -83,10 +83,10 @@
)
from opentrons.protocol_engine.state.geometry import GeometryView, _GripperMoveType
from opentrons.protocol_engine.state.frustum_helpers import (
- height_from_volume_circular,
- height_from_volume_rectangular,
- volume_from_height_circular,
- volume_from_height_rectangular,
+ _height_from_volume_circular,
+ _height_from_volume_rectangular,
+ _volume_from_height_circular,
+ _volume_from_height_rectangular,
)
from ..pipette_fixtures import get_default_nozzle_map
from ..mock_circular_frusta import TEST_EXAMPLES as CIRCULAR_TEST_EXAMPLES
@@ -2776,7 +2776,7 @@ def _find_volume_from_height_(index: int) -> None:
top_width = frustum["width"][index]
target_height = frustum["height"][index]
- found_volume = volume_from_height_rectangular(
+ found_volume = _volume_from_height_rectangular(
target_height=target_height,
total_frustum_height=total_frustum_height,
top_length=top_length,
@@ -2785,7 +2785,7 @@ def _find_volume_from_height_(index: int) -> None:
bottom_width=bottom_width,
)
- found_height = height_from_volume_rectangular(
+ found_height = _height_from_volume_rectangular(
volume=found_volume,
total_frustum_height=total_frustum_height,
top_length=top_length,
@@ -2815,14 +2815,14 @@ def _find_volume_from_height_(index: int) -> None:
top_radius = frustum["radius"][index]
target_height = frustum["height"][index]
- found_volume = volume_from_height_circular(
+ found_volume = _volume_from_height_circular(
target_height=target_height,
total_frustum_height=total_frustum_height,
top_radius=top_radius,
bottom_radius=bottom_radius,
)
- found_height = height_from_volume_circular(
+ found_height = _height_from_volume_circular(
volume=found_volume,
total_frustum_height=total_frustum_height,
top_radius=top_radius,
diff --git a/api/tests/opentrons/protocol_runner/test_json_translator.py b/api/tests/opentrons/protocol_runner/test_json_translator.py
index a583fcbf1c4..afaf105f347 100644
--- a/api/tests/opentrons/protocol_runner/test_json_translator.py
+++ b/api/tests/opentrons/protocol_runner/test_json_translator.py
@@ -13,7 +13,7 @@
Group,
Metadata1,
WellDefinition,
- RectangularBoundedSection,
+ CuboidalFrustum,
InnerWellGeometry,
SphericalSegment,
)
@@ -685,32 +685,39 @@ def _load_labware_definition_data() -> LabwareDefinition:
y=75.43,
z=75,
totalLiquidVolume=1100000,
- shape="rectangular",
+ shape="circular",
)
},
dimensions=Dimensions(yDimension=85.5, zDimension=100, xDimension=127.75),
cornerOffsetFromSlot=CornerOffsetFromSlot(x=0, y=0, z=0),
innerLabwareGeometry={
"welldefinition1111": InnerWellGeometry(
- frusta=[
- RectangularBoundedSection(
- shape="rectangular",
- xDimension=7.6,
- yDimension=8.5,
+ sections=[
+ CuboidalFrustum(
+ shape="cuboidal",
+ topXDimension=7.6,
+ topYDimension=8.5,
+ bottomXDimension=5.6,
+ bottomYDimension=6.5,
topHeight=45,
+ bottomHeight=20,
),
- RectangularBoundedSection(
- shape="rectangular",
- xDimension=5.6,
- yDimension=6.5,
+ CuboidalFrustum(
+ shape="cuboidal",
+ topXDimension=5.6,
+ topYDimension=6.5,
+ bottomXDimension=4.5,
+ bottomYDimension=4.0,
topHeight=20,
+ bottomHeight=10,
+ ),
+ SphericalSegment(
+ shape="spherical",
+ radiusOfCurvature=6,
+ topHeight=10,
+ bottomHeight=0.0,
),
],
- bottomShape=SphericalSegment(
- shape="spherical",
- radiusOfCurvature=6,
- depth=10,
- ),
)
},
brand=BrandData(brand="foo"),
diff --git a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py
index 0bf74aae5b2..0b8d3429527 100644
--- a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py
+++ b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py
@@ -2,24 +2,25 @@
from math import pi, isclose
from typing import Any, List
-from opentrons_shared_data.labware.types import (
- RectangularBoundedSection,
- CircularBoundedSection,
+from opentrons_shared_data.labware.labware_definition import (
+ ConicalFrustum,
+ CuboidalFrustum,
SphericalSegment,
)
from opentrons.protocol_engine.state.frustum_helpers import (
- cross_section_area_rectangular,
- cross_section_area_circular,
- reject_unacceptable_heights,
- get_boundary_pairs,
- circular_frustum_polynomial_roots,
- rectangular_frustum_polynomial_roots,
- volume_from_height_rectangular,
- volume_from_height_circular,
- volume_from_height_spherical,
- height_from_volume_circular,
- height_from_volume_rectangular,
- height_from_volume_spherical,
+ _cross_section_area_rectangular,
+ _cross_section_area_circular,
+ _reject_unacceptable_heights,
+ _circular_frustum_polynomial_roots,
+ _rectangular_frustum_polynomial_roots,
+ _volume_from_height_rectangular,
+ _volume_from_height_circular,
+ _volume_from_height_spherical,
+ _height_from_volume_circular,
+ _height_from_volume_rectangular,
+ _height_from_volume_spherical,
+ height_at_volume_within_section,
+ _get_segment_capacity,
)
from opentrons.protocol_engine.errors.exceptions import InvalidLiquidHeightFound
@@ -29,59 +30,130 @@ def fake_frusta() -> List[List[Any]]:
frusta = []
frusta.append(
[
- RectangularBoundedSection(
- shape="rectangular", xDimension=9.0, yDimension=10.0, topHeight=10.0
+ CuboidalFrustum(
+ shape="cuboidal",
+ topXDimension=9.0,
+ topYDimension=10.0,
+ bottomXDimension=8.0,
+ bottomYDimension=9.0,
+ topHeight=10.0,
+ bottomHeight=5.0,
),
- RectangularBoundedSection(
- shape="rectangular", xDimension=8.0, yDimension=9.0, topHeight=5.0
+ CuboidalFrustum(
+ shape="cuboidal",
+ topXDimension=8.0,
+ topYDimension=9.0,
+ bottomXDimension=15.0,
+ bottomYDimension=18.0,
+ topHeight=5.0,
+ bottomHeight=1.0,
+ ),
+ ConicalFrustum(
+ shape="conical",
+ topDiameter=23.0,
+ bottomDiameter=3.0,
+ topHeight=2.0,
+ bottomHeight=1.0,
+ ),
+ SphericalSegment(
+ shape="spherical",
+ radiusOfCurvature=4.0,
+ topHeight=1.0,
+ bottomHeight=0.0,
),
- CircularBoundedSection(shape="circular", diameter=23.0, topHeight=1.0),
- SphericalSegment(shape="spherical", radiusOfCurvature=4.0, depth=1.0),
]
)
frusta.append(
[
- RectangularBoundedSection(
- shape="rectangular", xDimension=8.0, yDimension=70.0, topHeight=3.5
- ),
- RectangularBoundedSection(
- shape="rectangular", xDimension=8.0, yDimension=75.0, topHeight=2.0
+ CuboidalFrustum(
+ shape="cuboidal",
+ topXDimension=8.0,
+ topYDimension=70.0,
+ bottomXDimension=7.0,
+ bottomYDimension=75.0,
+ topHeight=3.5,
+ bottomHeight=2.0,
),
- RectangularBoundedSection(
- shape="rectangular", xDimension=8.0, yDimension=80.0, topHeight=1.0
- ),
- RectangularBoundedSection(
- shape="rectangular", xDimension=8.0, yDimension=90.0, topHeight=0.0
+ CuboidalFrustum(
+ shape="cuboidal",
+ topXDimension=8.0,
+ topYDimension=80.0,
+ bottomXDimension=8.0,
+ bottomYDimension=90.0,
+ topHeight=1.0,
+ bottomHeight=0.0,
),
]
)
frusta.append(
[
- CircularBoundedSection(shape="circular", diameter=23.0, topHeight=7.5),
- CircularBoundedSection(shape="circular", diameter=11.5, topHeight=5.0),
- CircularBoundedSection(shape="circular", diameter=23.0, topHeight=2.5),
- CircularBoundedSection(shape="circular", diameter=11.5, topHeight=0.0),
+ ConicalFrustum(
+ shape="conical",
+ topDiameter=23.0,
+ bottomDiameter=11.5,
+ topHeight=7.5,
+ bottomHeight=5.0,
+ ),
+ ConicalFrustum(
+ shape="conical",
+ topDiameter=11.5,
+ bottomDiameter=23.0,
+ topHeight=5.0,
+ bottomHeight=2.5,
+ ),
+ ConicalFrustum(
+ shape="conical",
+ topDiameter=23.0,
+ bottomDiameter=11.5,
+ topHeight=2.5,
+ bottomHeight=0.0,
+ ),
]
)
frusta.append(
[
- CircularBoundedSection(shape="circular", diameter=4.0, topHeight=3.0),
- CircularBoundedSection(shape="circular", diameter=5.0, topHeight=2.0),
- SphericalSegment(shape="spherical", radiusOfCurvature=3.5, depth=2.0),
+ ConicalFrustum(
+ shape="conical",
+ topDiameter=4.0,
+ bottomDiameter=5.0,
+ topHeight=3.0,
+ bottomHeight=2.0,
+ ),
+ SphericalSegment(
+ shape="spherical",
+ radiusOfCurvature=3.5,
+ topHeight=2.0,
+ bottomHeight=0.0,
+ ),
]
)
frusta.append(
- [SphericalSegment(shape="spherical", radiusOfCurvature=4.0, depth=3.0)]
+ [
+ SphericalSegment(
+ shape="spherical",
+ radiusOfCurvature=4.0,
+ topHeight=3.0,
+ bottomHeight=0.0,
+ )
+ ]
)
frusta.append(
[
- RectangularBoundedSection(
- shape="rectangular", xDimension=27.0, yDimension=36.0, topHeight=3.5
+ CuboidalFrustum(
+ shape="cuboidal",
+ topXDimension=27.0,
+ topYDimension=36.0,
+ bottomXDimension=36.0,
+ bottomYDimension=26.0,
+ topHeight=3.5,
+ bottomHeight=1.5,
),
- RectangularBoundedSection(
- shape="rectangular", xDimension=36.0, yDimension=26.0, topHeight=1.5
+ SphericalSegment(
+ shape="spherical",
+ radiusOfCurvature=4.0,
+ topHeight=1.5,
+ bottomHeight=0.0,
),
- SphericalSegment(shape="spherical", radiusOfCurvature=4.0, depth=1.5),
]
)
return frusta
@@ -103,11 +175,11 @@ def test_reject_unacceptable_heights(
"""Make sure we reject all mathematical solutions that are physically not possible."""
if len(expected_heights) != 1:
with pytest.raises(InvalidLiquidHeightFound):
- reject_unacceptable_heights(
+ _reject_unacceptable_heights(
max_height=max_height, potential_heights=potential_heights
)
else:
- found_heights = reject_unacceptable_heights(
+ found_heights = _reject_unacceptable_heights(
max_height=max_height, potential_heights=potential_heights
)
assert found_heights == expected_heights[0]
@@ -117,7 +189,7 @@ def test_reject_unacceptable_heights(
def test_cross_section_area_circular(diameter: float) -> None:
"""Test circular area calculation."""
expected_area = pi * (diameter / 2) ** 2
- assert cross_section_area_circular(diameter) == expected_area
+ assert _cross_section_area_circular(diameter) == expected_area
@pytest.mark.parametrize(
@@ -127,35 +199,27 @@ def test_cross_section_area_rectangular(x_dimension: float, y_dimension: float)
"""Test rectangular area calculation."""
expected_area = x_dimension * y_dimension
assert (
- cross_section_area_rectangular(x_dimension=x_dimension, y_dimension=y_dimension)
+ _cross_section_area_rectangular(
+ x_dimension=x_dimension, y_dimension=y_dimension
+ )
== expected_area
)
-@pytest.mark.parametrize("well", fake_frusta())
-def test_get_cross_section_boundaries(well: List[List[Any]]) -> None:
- """Make sure get_cross_section_boundaries returns the expected list indices."""
- i = 0
- for f, next_f in get_boundary_pairs(well):
- assert f == well[i]
- assert next_f == well[i + 1]
- i += 1
-
-
@pytest.mark.parametrize("well", fake_frusta())
def test_volume_and_height_circular(well: List[Any]) -> None:
"""Test both volume and height calculations for circular frusta."""
- if well[-1]["shape"] == "spherical":
+ if well[-1].shape == "spherical":
return
- total_height = well[0]["topHeight"]
- for f, next_f in get_boundary_pairs(well):
- if f["shape"] == next_f["shape"] == "circular":
- top_radius = next_f["diameter"] / 2
- bottom_radius = f["diameter"] / 2
+ total_height = well[0].topHeight
+ for segment in well:
+ if segment.shape == "conical":
+ top_radius = segment.topDiameter / 2
+ bottom_radius = segment.bottomDiameter / 2
a = pi * ((top_radius - bottom_radius) ** 2) / (3 * total_height**2)
b = pi * bottom_radius * (top_radius - bottom_radius) / total_height
c = pi * bottom_radius**2
- assert circular_frustum_polynomial_roots(
+ assert _circular_frustum_polynomial_roots(
top_radius=top_radius,
bottom_radius=bottom_radius,
total_frustum_height=total_height,
@@ -167,7 +231,7 @@ def test_volume_and_height_circular(well: List[Any]) -> None:
+ b * (target_height**2)
+ c * target_height
)
- found_volume = volume_from_height_circular(
+ found_volume = _volume_from_height_circular(
target_height=target_height,
total_frustum_height=total_height,
bottom_radius=bottom_radius,
@@ -175,7 +239,7 @@ def test_volume_and_height_circular(well: List[Any]) -> None:
)
assert found_volume == expected_volume
# test going backwards to get height back
- found_height = height_from_volume_circular(
+ found_height = _height_from_volume_circular(
volume=found_volume,
total_frustum_height=total_height,
bottom_radius=bottom_radius,
@@ -187,15 +251,15 @@ def test_volume_and_height_circular(well: List[Any]) -> None:
@pytest.mark.parametrize("well", fake_frusta())
def test_volume_and_height_rectangular(well: List[Any]) -> None:
"""Test both volume and height calculations for rectangular frusta."""
- if well[-1]["shape"] == "spherical":
+ if well[-1].shape == "spherical":
return
- total_height = well[0]["topHeight"]
- for f, next_f in get_boundary_pairs(well):
- if f["shape"] == next_f["shape"] == "rectangular":
- top_length = next_f["yDimension"]
- top_width = next_f["xDimension"]
- bottom_length = f["yDimension"]
- bottom_width = f["xDimension"]
+ total_height = well[0].topHeight
+ for segment in well:
+ if segment.shape == "cuboidal":
+ top_length = segment.topYDimension
+ top_width = segment.topXDimension
+ bottom_length = segment.bottomYDimension
+ bottom_width = segment.bottomXDimension
a = (
(top_length - bottom_length)
* (top_width - bottom_width)
@@ -206,7 +270,7 @@ def test_volume_and_height_rectangular(well: List[Any]) -> None:
+ (bottom_width * (top_length - bottom_length))
) / (2 * total_height)
c = bottom_length * bottom_width
- assert rectangular_frustum_polynomial_roots(
+ assert _rectangular_frustum_polynomial_roots(
top_length=top_length,
bottom_length=bottom_length,
top_width=top_width,
@@ -220,7 +284,7 @@ def test_volume_and_height_rectangular(well: List[Any]) -> None:
+ b * (target_height**2)
+ c * target_height
)
- found_volume = volume_from_height_rectangular(
+ found_volume = _volume_from_height_rectangular(
target_height=target_height,
total_frustum_height=total_height,
bottom_length=bottom_length,
@@ -230,7 +294,7 @@ def test_volume_and_height_rectangular(well: List[Any]) -> None:
)
assert found_volume == expected_volume
# test going backwards to get height back
- found_height = height_from_volume_rectangular(
+ found_height = _height_from_volume_rectangular(
volume=found_volume,
total_frustum_height=total_height,
bottom_length=bottom_length,
@@ -244,22 +308,33 @@ def test_volume_and_height_rectangular(well: List[Any]) -> None:
@pytest.mark.parametrize("well", fake_frusta())
def test_volume_and_height_spherical(well: List[Any]) -> None:
"""Test both volume and height calculations for spherical segments."""
- if well[0]["shape"] == "spherical":
- for target_height in range(round(well[0]["depth"])):
+ if well[0].shape == "spherical":
+ for target_height in range(round(well[0].topHeight)):
expected_volume = (
(1 / 3)
* pi
* (target_height**2)
- * (3 * well[0]["radiusOfCurvature"] - target_height)
+ * (3 * well[0].radiusOfCurvature - target_height)
)
- found_volume = volume_from_height_spherical(
+ found_volume = _volume_from_height_spherical(
target_height=target_height,
- radius_of_curvature=well[0]["radiusOfCurvature"],
+ radius_of_curvature=well[0].radiusOfCurvature,
)
assert found_volume == expected_volume
- found_height = height_from_volume_spherical(
+ found_height = _height_from_volume_spherical(
volume=found_volume,
- radius_of_curvature=well[0]["radiusOfCurvature"],
- total_frustum_height=well[0]["depth"],
+ radius_of_curvature=well[0].radiusOfCurvature,
+ total_frustum_height=well[0].topHeight,
)
assert isclose(found_height, target_height)
+
+
+@pytest.mark.parametrize("well", fake_frusta())
+def test_height_at_volume_within_section(well: List[Any]) -> None:
+ """Test that finding the height when volume ~= capacity works."""
+ for segment in well:
+ segment_height = segment.topHeight - segment.bottomHeight
+ height = height_at_volume_within_section(
+ segment, _get_segment_capacity(segment), segment_height
+ )
+ assert isclose(height, segment_height)
diff --git a/components/src/organisms/Toolbox/index.tsx b/components/src/organisms/Toolbox/index.tsx
index 1a6cb435a9e..566bcf1e4bf 100644
--- a/components/src/organisms/Toolbox/index.tsx
+++ b/components/src/organisms/Toolbox/index.tsx
@@ -119,7 +119,6 @@ export function Toolbox(props: ToolboxProps): JSX.Element {
-
str
idx_str = ""
for i, p in enumerate(ports):
print(f"\t{i + 1}) {p.device}")
- if port_substr:
- for i, p in enumerate(ports):
- if port_substr in p.device:
- idx = i + 1
- break
- else:
- idx_str = input(
- f"\nenter number next to {device_name} port (or ENTER to re-scan): "
- )
- if not idx_str:
- return list_ports_and_select(device_name)
- if not device_name:
- device_name = "desired"
-
- try:
+ if port_substr:
+ for i, p in enumerate(ports):
+ if port_substr in p.device:
+ return p.device
+
+ while True:
+ idx_str = input(
+ f"\nEnter number next to {device_name} port (or ENTER to re-scan): "
+ )
+ if not idx_str:
+ return list_ports_and_select(device_name, port_substr)
+
try:
idx = int(idx_str.strip())
- except TypeError:
- pass
- return ports[idx - 1].device
- except (ValueError, IndexError):
- return list_ports_and_select()
+ return ports[idx - 1].device
+ except (ValueError, IndexError):
+ print("Invalid selection. Please try again.")
def find_port(vid: int, pid: int) -> str:
diff --git a/hardware-testing/hardware_testing/gravimetric/helpers.py b/hardware-testing/hardware_testing/gravimetric/helpers.py
index eaadca2c6a9..b3533a002b0 100644
--- a/hardware-testing/hardware_testing/gravimetric/helpers.py
+++ b/hardware-testing/hardware_testing/gravimetric/helpers.py
@@ -41,7 +41,7 @@
WellLocation,
DropTipWellLocation,
)
-from opentrons.protocol_api.core.engine import deck_conflict as DeckConflit
+from opentrons.protocol_api.core.engine import pipette_movement_conflict
def _add_fake_simulate(
@@ -455,8 +455,8 @@ def _load_pipette(
front_right_nozzle="A1",
back_left_nozzle="A1",
)
- # override deck conflict checking cause we specially lay out our tipracks
- DeckConflit.check_safe_for_pipette_movement = (
+ # override pipette movement conflict checking 'cause we specially lay out our tipracks
+ pipette_movement_conflict.check_safe_for_pipette_movement = (
_override_check_safe_for_pipette_movement
)
pipette.trash_container = trash
diff --git a/hardware-testing/hardware_testing/scripts/abr_asair_sensor.py b/hardware-testing/hardware_testing/scripts/abr_asair_sensor.py
index f2c00e015d3..1e8fca0358c 100644
--- a/hardware-testing/hardware_testing/scripts/abr_asair_sensor.py
+++ b/hardware-testing/hardware_testing/scripts/abr_asair_sensor.py
@@ -26,7 +26,7 @@ def __init__(self, robot: str, duration: int, frequency: int) -> None:
test_name = "ABR-Environment-Monitoring"
run_id = data.create_run_id()
file_name = data.create_file_name(test_name, run_id, robot)
- sensor = asair_sensor.BuildAsairSensor(False, False, "USB")
+ sensor = asair_sensor.BuildAsairSensor(False, False, "USB0")
print(sensor)
env_data = sensor.get_reading()
header = [
diff --git a/opentrons-ai-server/Dockerfile b/opentrons-ai-server/Dockerfile
index ddd19bb88c7..7a9a696145b 100644
--- a/opentrons-ai-server/Dockerfile
+++ b/opentrons-ai-server/Dockerfile
@@ -1,7 +1,8 @@
-FROM --platform=linux/amd64 python:3.12-slim
+ARG PLATFORM=linux/amd64
+FROM --platform=$PLATFORM python:3.12-slim
-ENV PYTHONUNBUFFERED True
-ENV DOCKER_RUNNING True
+ENV PYTHONUNBUFFERED=True
+ENV DOCKER_RUNNING=True
WORKDIR /code
@@ -15,4 +16,4 @@ COPY ./api /code/api
EXPOSE 8000
-CMD ["ddtrace-run", "uvicorn", "api.handler.fast:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "8000", "--timeout-keep-alive", "190", "--workers", "3"]
+CMD ["ddtrace-run", "uvicorn", "api.handler.fast:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "8000", "--timeout-keep-alive", "190", "--log-config", "/code/api/uvicorn_disable_logging.json", "--workers", "3"]
diff --git a/opentrons-ai-server/Makefile b/opentrons-ai-server/Makefile
index e3f678606e1..60eba38a312 100644
--- a/opentrons-ai-server/Makefile
+++ b/opentrons-ai-server/Makefile
@@ -115,7 +115,7 @@ run:
docker logs -f $(CONTAINER_NAME)
.PHONY: clean
-clean:
+clean: gen-requirements
docker stop $(CONTAINER_NAME) || true
docker rm $(CONTAINER_NAME) || true
diff --git a/opentrons-ai-server/Pipfile b/opentrons-ai-server/Pipfile
index a6ee65a0160..34b0b8d32dd 100644
--- a/opentrons-ai-server/Pipfile
+++ b/opentrons-ai-server/Pipfile
@@ -13,9 +13,10 @@ fastapi = "==0.111.0"
ddtrace = "==2.9.2"
pydantic-settings = "==2.3.4"
pyjwt = {extras = ["crypto"], version = "*"}
-python-json-logger = "==2.0.7"
beautifulsoup4 = "==4.12.3"
markdownify = "==0.13.1"
+structlog = "==24.4.0"
+asgi-correlation-id = "==4.3.3"
[dev-packages]
docker = "==7.1.0"
diff --git a/opentrons-ai-server/Pipfile.lock b/opentrons-ai-server/Pipfile.lock
index 8861f152e8e..55811db04cf 100644
--- a/opentrons-ai-server/Pipfile.lock
+++ b/opentrons-ai-server/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "7526bc1898bd03e19a277baf44f56d6f1287870288af558c8d8b719118af3389"
+ "sha256": "20b9e324d809f68cb0465d5e3d98467ceb5860f583fddc347ade1e5ad6a3b6ab"
},
"pipfile-spec": 6,
"requires": {
@@ -18,108 +18,108 @@
"default": {
"aiohappyeyeballs": {
"hashes": [
- "sha256:55a1714f084e63d49639800f95716da97a1f173d46a16dfcfda0016abb93b6b2",
- "sha256:7ce92076e249169a13c2f49320d1967425eaf1f407522d707d59cac7628d62bd"
+ "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586",
+ "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572"
],
"markers": "python_version >= '3.8'",
- "version": "==2.4.0"
+ "version": "==2.4.3"
},
"aiohttp": {
"hashes": [
- "sha256:02108326574ff60267b7b35b17ac5c0bbd0008ccb942ce4c48b657bb90f0b8aa",
- "sha256:029a019627b37fa9eac5c75cc54a6bb722c4ebbf5a54d8c8c0fb4dd8facf2702",
- "sha256:03fa40d1450ee5196e843315ddf74a51afc7e83d489dbfc380eecefea74158b1",
- "sha256:0749c4d5a08a802dd66ecdf59b2df4d76b900004017468a7bb736c3b5a3dd902",
- "sha256:0754690a3a26e819173a34093798c155bafb21c3c640bff13be1afa1e9d421f9",
- "sha256:0a75d5c9fb4f06c41d029ae70ad943c3a844c40c0a769d12be4b99b04f473d3d",
- "sha256:0b82c8ebed66ce182893e7c0b6b60ba2ace45b1df104feb52380edae266a4850",
- "sha256:0be3115753baf8b4153e64f9aa7bf6c0c64af57979aa900c31f496301b374570",
- "sha256:14477c4e52e2f17437b99893fd220ffe7d7ee41df5ebf931a92b8ca82e6fd094",
- "sha256:164ecd32e65467d86843dbb121a6666c3deb23b460e3f8aefdcaacae79eb718a",
- "sha256:1cb045ec5961f51af3e2c08cd6fe523f07cc6e345033adee711c49b7b91bb954",
- "sha256:1e52e59ed5f4cc3a3acfe2a610f8891f216f486de54d95d6600a2c9ba1581f4d",
- "sha256:217791c6a399cc4f2e6577bb44344cba1f5714a2aebf6a0bea04cfa956658284",
- "sha256:25d92f794f1332f656e3765841fc2b7ad5c26c3f3d01e8949eeb3495691cf9f4",
- "sha256:2708baccdc62f4b1251e59c2aac725936a900081f079b88843dabcab0feeeb27",
- "sha256:27cf19a38506e2e9f12fc17e55f118f04897b0a78537055d93a9de4bf3022e3d",
- "sha256:289fa8a20018d0d5aa9e4b35d899bd51bcb80f0d5f365d9a23e30dac3b79159b",
- "sha256:2cd5290ab66cfca2f90045db2cc6434c1f4f9fbf97c9f1c316e785033782e7d2",
- "sha256:2dd56e3c43660ed3bea67fd4c5025f1ac1f9ecf6f0b991a6e5efe2e678c490c5",
- "sha256:3427031064b0d5c95647e6369c4aa3c556402f324a3e18107cb09517abe5f962",
- "sha256:3468b39f977a11271517c6925b226720e148311039a380cc9117b1e2258a721f",
- "sha256:370e2d47575c53c817ee42a18acc34aad8da4dbdaac0a6c836d58878955f1477",
- "sha256:3d2665c5df629eb2f981dab244c01bfa6cdc185f4ffa026639286c4d56fafb54",
- "sha256:3e15e33bfc73fa97c228f72e05e8795e163a693fd5323549f49367c76a6e5883",
- "sha256:3fb4216e3ec0dbc01db5ba802f02ed78ad8f07121be54eb9e918448cc3f61b7c",
- "sha256:40271a2a375812967401c9ca8077de9368e09a43a964f4dce0ff603301ec9358",
- "sha256:438c5863feb761f7ca3270d48c292c334814459f61cc12bab5ba5b702d7c9e56",
- "sha256:4407a80bca3e694f2d2a523058e20e1f9f98a416619e04f6dc09dc910352ac8b",
- "sha256:444d1704e2af6b30766debed9be8a795958029e552fe77551355badb1944012c",
- "sha256:4611db8c907f90fe86be112efdc2398cd7b4c8eeded5a4f0314b70fdea8feab0",
- "sha256:473961b3252f3b949bb84873d6e268fb6d8aa0ccc6eb7404fa58c76a326bb8e1",
- "sha256:4752df44df48fd42b80f51d6a97553b482cda1274d9dc5df214a3a1aa5d8f018",
- "sha256:47647c8af04a70e07a2462931b0eba63146a13affa697afb4ecbab9d03a480ce",
- "sha256:482f74057ea13d387a7549d7a7ecb60e45146d15f3e58a2d93a0ad2d5a8457cd",
- "sha256:4bef1480ee50f75abcfcb4b11c12de1005968ca9d0172aec4a5057ba9f2b644f",
- "sha256:4fabdcdc781a36b8fd7b2ca9dea8172f29a99e11d00ca0f83ffeb50958da84a1",
- "sha256:5582de171f0898139cf51dd9fcdc79b848e28d9abd68e837f0803fc9f30807b1",
- "sha256:58c5d7318a136a3874c78717dd6de57519bc64f6363c5827c2b1cb775bea71dd",
- "sha256:5db26bbca8e7968c4c977a0c640e0b9ce7224e1f4dcafa57870dc6ee28e27de6",
- "sha256:614fc21e86adc28e4165a6391f851a6da6e9cbd7bb232d0df7718b453a89ee98",
- "sha256:6419728b08fb6380c66a470d2319cafcec554c81780e2114b7e150329b9a9a7f",
- "sha256:669c0efe7e99f6d94d63274c06344bd0e9c8daf184ce5602a29bc39e00a18720",
- "sha256:66bc81361131763660b969132a22edce2c4d184978ba39614e8f8f95db5c95f8",
- "sha256:671745ea7db19693ce867359d503772177f0b20fa8f6ee1e74e00449f4c4151d",
- "sha256:682836fc672972cc3101cc9e30d49c5f7e8f1d010478d46119fe725a4545acfd",
- "sha256:6a504d7cdb431a777d05a124fd0b21efb94498efa743103ea01b1e3136d2e4fb",
- "sha256:6a86610174de8a85a920e956e2d4f9945e7da89f29a00e95ac62a4a414c4ef4e",
- "sha256:6b50b367308ca8c12e0b50cba5773bc9abe64c428d3fd2bbf5cd25aab37c77bf",
- "sha256:7475da7a5e2ccf1a1c86c8fee241e277f4874c96564d06f726d8df8e77683ef7",
- "sha256:7641920bdcc7cd2d3ddfb8bb9133a6c9536b09dbd49490b79e125180b2d25b93",
- "sha256:79a9f42efcc2681790595ab3d03c0e52d01edc23a0973ea09f0dc8d295e12b8e",
- "sha256:7ea35d849cdd4a9268f910bff4497baebbc1aa3f2f625fd8ccd9ac99c860c621",
- "sha256:8198b7c002aae2b40b2d16bfe724b9a90bcbc9b78b2566fc96131ef4e382574d",
- "sha256:81b292f37969f9cc54f4643f0be7dacabf3612b3b4a65413661cf6c350226787",
- "sha256:844d48ff9173d0b941abed8b2ea6a412f82b56d9ab1edb918c74000c15839362",
- "sha256:8617c96a20dd57e7e9d398ff9d04f3d11c4d28b1767273a5b1a018ada5a654d3",
- "sha256:8a637d387db6fdad95e293fab5433b775fd104ae6348d2388beaaa60d08b38c4",
- "sha256:92351aa5363fc3c1f872ca763f86730ced32b01607f0c9662b1fa711087968d0",
- "sha256:9843d683b8756971797be171ead21511d2215a2d6e3c899c6e3107fbbe826791",
- "sha256:995ab1a238fd0d19dc65f2d222e5eb064e409665c6426a3e51d5101c1979ee84",
- "sha256:9bd6b2033993d5ae80883bb29b83fb2b432270bbe067c2f53cc73bb57c46065f",
- "sha256:9d26da22a793dfd424be1050712a70c0afd96345245c29aced1e35dbace03413",
- "sha256:a976ef488f26e224079deb3d424f29144c6d5ba4ded313198169a8af8f47fb82",
- "sha256:a9f196c970db2dcde4f24317e06615363349dc357cf4d7a3b0716c20ac6d7bcd",
- "sha256:b169f8e755e541b72e714b89a831b315bbe70db44e33fead28516c9e13d5f931",
- "sha256:b504c08c45623bf5c7ca41be380156d925f00199b3970efd758aef4a77645feb",
- "sha256:ba18573bb1de1063d222f41de64a0d3741223982dcea863b3f74646faf618ec7",
- "sha256:ba3662d41abe2eab0eeec7ee56f33ef4e0b34858f38abf24377687f9e1fb00a5",
- "sha256:bd294dcdc1afdc510bb51d35444003f14e327572877d016d576ac3b9a5888a27",
- "sha256:bdbeff1b062751c2a2a55b171f7050fb7073633c699299d042e962aacdbe1a07",
- "sha256:bf861da9a43d282d6dd9dcd64c23a0fccf2c5aa5cd7c32024513c8c79fb69de3",
- "sha256:c82a94ddec996413a905f622f3da02c4359952aab8d817c01cf9915419525e95",
- "sha256:c91781d969fbced1993537f45efe1213bd6fccb4b37bfae2a026e20d6fbed206",
- "sha256:c9721cdd83a994225352ca84cd537760d41a9da3c0eacb3ff534747ab8fba6d0",
- "sha256:cca776a440795db437d82c07455761c85bbcf3956221c3c23b8c93176c278ce7",
- "sha256:cf8b8560aa965f87bf9c13bf9fed7025993a155ca0ce8422da74bf46d18c2f5f",
- "sha256:d2578ef941be0c2ba58f6f421a703527d08427237ed45ecb091fed6f83305336",
- "sha256:d2b3935a22c9e41a8000d90588bed96cf395ef572dbb409be44c6219c61d900d",
- "sha256:d4dfa5ad4bce9ca30a76117fbaa1c1decf41ebb6c18a4e098df44298941566f9",
- "sha256:d7f408c43f5e75ea1edc152fb375e8f46ef916f545fb66d4aebcbcfad05e2796",
- "sha256:dc1a16f3fc1944c61290d33c88dc3f09ba62d159b284c38c5331868425aca426",
- "sha256:e0009258e97502936d3bd5bf2ced15769629097d0abb81e6495fba1047824fe0",
- "sha256:e05b39158f2af0e2438cc2075cfc271f4ace0c3cc4a81ec95b27a0432e161951",
- "sha256:e1f80cd17d81a404b6e70ef22bfe1870bafc511728397634ad5f5efc8698df56",
- "sha256:e2e7d5591ea868d5ec82b90bbeb366a198715672841d46281b623e23079593db",
- "sha256:f3af26f86863fad12e25395805bb0babbd49d512806af91ec9708a272b696248",
- "sha256:f52e54fd776ad0da1006708762213b079b154644db54bcfc62f06eaa5b896402",
- "sha256:f8b8e49fe02f744d38352daca1dbef462c3874900bd8166516f6ea8e82b5aacf",
- "sha256:fb138fbf9f53928e779650f5ed26d0ea1ed8b2cab67f0ea5d63afa09fdc07593",
- "sha256:fe517113fe4d35d9072b826c3e147d63c5f808ca8167d450b4f96c520c8a1d8d",
- "sha256:ff99ae06eef85c7a565854826114ced72765832ee16c7e3e766c5e4c5b98d20e"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==3.10.6"
+ "sha256:02d1d6610588bcd743fae827bd6f2e47e0d09b346f230824b4c6fb85c6065f9c",
+ "sha256:03690541e4cc866eef79626cfa1ef4dd729c5c1408600c8cb9e12e1137eed6ab",
+ "sha256:0bc059ecbce835630e635879f5f480a742e130d9821fbe3d2f76610a6698ee25",
+ "sha256:0c21c82df33b264216abffff9f8370f303dab65d8eee3767efbbd2734363f677",
+ "sha256:1298b854fd31d0567cbb916091be9d3278168064fca88e70b8468875ef9ff7e7",
+ "sha256:1321658f12b6caffafdc35cfba6c882cb014af86bef4e78c125e7e794dfb927b",
+ "sha256:143b0026a9dab07a05ad2dd9e46aa859bffdd6348ddc5967b42161168c24f857",
+ "sha256:16e6a51d8bc96b77f04a6764b4ad03eeef43baa32014fce71e882bd71302c7e4",
+ "sha256:172ad884bb61ad31ed7beed8be776eb17e7fb423f1c1be836d5cb357a096bf12",
+ "sha256:17c272cfe7b07a5bb0c6ad3f234e0c336fb53f3bf17840f66bd77b5815ab3d16",
+ "sha256:1a0ee6c0d590c917f1b9629371fce5f3d3f22c317aa96fbdcce3260754d7ea21",
+ "sha256:2746d8994ebca1bdc55a1e998feff4e94222da709623bb18f6e5cfec8ec01baf",
+ "sha256:2914caa46054f3b5ff910468d686742ff8cff54b8a67319d75f5d5945fd0a13d",
+ "sha256:2bbf94d4a0447705b7775417ca8bb8086cc5482023a6e17cdc8f96d0b1b5aba6",
+ "sha256:2bd9f3eac515c16c4360a6a00c38119333901b8590fe93c3257a9b536026594d",
+ "sha256:2c33fa6e10bb7ed262e3ff03cc69d52869514f16558db0626a7c5c61dde3c29f",
+ "sha256:2d37f4718002863b82c6f391c8efd4d3a817da37030a29e2682a94d2716209de",
+ "sha256:3668d0c2a4d23fb136a753eba42caa2c0abbd3d9c5c87ee150a716a16c6deec1",
+ "sha256:36d4fba838be5f083f5490ddd281813b44d69685db910907636bc5dca6322316",
+ "sha256:40ff5b7660f903dc587ed36ef08a88d46840182d9d4b5694e7607877ced698a1",
+ "sha256:42775de0ca04f90c10c5c46291535ec08e9bcc4756f1b48f02a0657febe89b10",
+ "sha256:482c85cf3d429844396d939b22bc2a03849cb9ad33344689ad1c85697bcba33a",
+ "sha256:4e6cb75f8ddd9c2132d00bc03c9716add57f4beff1263463724f6398b813e7eb",
+ "sha256:4edc3fd701e2b9a0d605a7b23d3de4ad23137d23fc0dbab726aa71d92f11aaaf",
+ "sha256:4fd16b30567c5b8e167923be6e027eeae0f20cf2b8a26b98a25115f28ad48ee0",
+ "sha256:5002a02c17fcfd796d20bac719981d2fca9c006aac0797eb8f430a58e9d12431",
+ "sha256:51d0a4901b27272ae54e42067bc4b9a90e619a690b4dc43ea5950eb3070afc32",
+ "sha256:558b3d223fd631ad134d89adea876e7fdb4c93c849ef195049c063ada82b7d08",
+ "sha256:5c070430fda1a550a1c3a4c2d7281d3b8cfc0c6715f616e40e3332201a253067",
+ "sha256:5f392ef50e22c31fa49b5a46af7f983fa3f118f3eccb8522063bee8bfa6755f8",
+ "sha256:60555211a006d26e1a389222e3fab8cd379f28e0fbf7472ee55b16c6c529e3a6",
+ "sha256:608cecd8d58d285bfd52dbca5b6251ca8d6ea567022c8a0eaae03c2589cd9af9",
+ "sha256:60ad5b8a7452c0f5645c73d4dad7490afd6119d453d302cd5b72b678a85d6044",
+ "sha256:63649309da83277f06a15bbdc2a54fbe75efb92caa2c25bb57ca37762789c746",
+ "sha256:6ebdc3b3714afe1b134b3bbeb5f745eed3ecbcff92ab25d80e4ef299e83a5465",
+ "sha256:6f3c6648aa123bcd73d6f26607d59967b607b0da8ffcc27d418a4b59f4c98c7c",
+ "sha256:7003f33f5f7da1eb02f0446b0f8d2ccf57d253ca6c2e7a5732d25889da82b517",
+ "sha256:776e9f3c9b377fcf097c4a04b241b15691e6662d850168642ff976780609303c",
+ "sha256:85711eec2d875cd88c7eb40e734c4ca6d9ae477d6f26bd2b5bb4f7f60e41b156",
+ "sha256:87d1e4185c5d7187684d41ebb50c9aeaaaa06ca1875f4c57593071b0409d2444",
+ "sha256:8a3f063b41cc06e8d0b3fcbbfc9c05b7420f41287e0cd4f75ce0a1f3d80729e6",
+ "sha256:8b3fb28a9ac8f2558760d8e637dbf27aef1e8b7f1d221e8669a1074d1a266bb2",
+ "sha256:8bd9125dd0cc8ebd84bff2be64b10fdba7dc6fd7be431b5eaf67723557de3a31",
+ "sha256:8be1a65487bdfc285bd5e9baf3208c2132ca92a9b4020e9f27df1b16fab998a9",
+ "sha256:8cc0d13b4e3b1362d424ce3f4e8c79e1f7247a00d792823ffd640878abf28e56",
+ "sha256:8d9d10d10ec27c0d46ddaecc3c5598c4db9ce4e6398ca872cdde0525765caa2f",
+ "sha256:8debb45545ad95b58cc16c3c1cc19ad82cffcb106db12b437885dbee265f0ab5",
+ "sha256:91aa966858593f64c8a65cdefa3d6dc8fe3c2768b159da84c1ddbbb2c01ab4ef",
+ "sha256:9331dd34145ff105177855017920dde140b447049cd62bb589de320fd6ddd582",
+ "sha256:99f9678bf0e2b1b695e8028fedac24ab6770937932eda695815d5a6618c37e04",
+ "sha256:9fdf5c839bf95fc67be5794c780419edb0dbef776edcfc6c2e5e2ffd5ee755fa",
+ "sha256:a14e4b672c257a6b94fe934ee62666bacbc8e45b7876f9dd9502d0f0fe69db16",
+ "sha256:a19caae0d670771ea7854ca30df76f676eb47e0fd9b2ee4392d44708f272122d",
+ "sha256:a35ed3d03910785f7d9d6f5381f0c24002b2b888b298e6f941b2fc94c5055fcd",
+ "sha256:a61df62966ce6507aafab24e124e0c3a1cfbe23c59732987fc0fd0d71daa0b88",
+ "sha256:a6e00c8a92e7663ed2be6fcc08a2997ff06ce73c8080cd0df10cc0321a3168d7",
+ "sha256:ac3196952c673822ebed8871cf8802e17254fff2a2ed4835d9c045d9b88c5ec7",
+ "sha256:ac74e794e3aee92ae8f571bfeaa103a141e409863a100ab63a253b1c53b707eb",
+ "sha256:ad3675c126f2a95bde637d162f8231cff6bc0bc9fbe31bd78075f9ff7921e322",
+ "sha256:aeebd3061f6f1747c011e1d0b0b5f04f9f54ad1a2ca183e687e7277bef2e0da2",
+ "sha256:ba1a599255ad6a41022e261e31bc2f6f9355a419575b391f9655c4d9e5df5ff5",
+ "sha256:bbdb8def5268f3f9cd753a265756f49228a20ed14a480d151df727808b4531dd",
+ "sha256:c2555e4949c8d8782f18ef20e9d39730d2656e218a6f1a21a4c4c0b56546a02e",
+ "sha256:c2695c61cf53a5d4345a43d689f37fc0f6d3a2dc520660aec27ec0f06288d1f9",
+ "sha256:c2b627d3c8982691b06d89d31093cee158c30629fdfebe705a91814d49b554f8",
+ "sha256:c46131c6112b534b178d4e002abe450a0a29840b61413ac25243f1291613806a",
+ "sha256:c54dc329cd44f7f7883a9f4baaefe686e8b9662e2c6c184ea15cceee587d8d69",
+ "sha256:c7d7cafc11d70fdd8801abfc2ff276744ae4cb39d8060b6b542c7e44e5f2cfc2",
+ "sha256:cb0b2d5d51f96b6cc19e6ab46a7b684be23240426ae951dcdac9639ab111b45e",
+ "sha256:d15a29424e96fad56dc2f3abed10a89c50c099f97d2416520c7a543e8fddf066",
+ "sha256:d1f5c9169e26db6a61276008582d945405b8316aae2bb198220466e68114a0f5",
+ "sha256:d271f770b52e32236d945911b2082f9318e90ff835d45224fa9e28374303f729",
+ "sha256:d646fdd74c25bbdd4a055414f0fe32896c400f38ffbdfc78c68e62812a9e0257",
+ "sha256:d6e395c3d1f773cf0651cd3559e25182eb0c03a2777b53b4575d8adc1149c6e9",
+ "sha256:d7c071235a47d407b0e93aa6262b49422dbe48d7d8566e1158fecc91043dd948",
+ "sha256:d97273a52d7f89a75b11ec386f786d3da7723d7efae3034b4dda79f6f093edc1",
+ "sha256:dcf354661f54e6a49193d0b5653a1b011ba856e0b7a76bda2c33e4c6892f34ea",
+ "sha256:e3e7fabedb3fe06933f47f1538df7b3a8d78e13d7167195f51ca47ee12690373",
+ "sha256:e525b69ee8a92c146ae5b4da9ecd15e518df4d40003b01b454ad694a27f498b5",
+ "sha256:e709d6ac598c5416f879bb1bae3fd751366120ac3fa235a01de763537385d036",
+ "sha256:e83dfefb4f7d285c2d6a07a22268344a97d61579b3e0dce482a5be0251d672ab",
+ "sha256:e86260b76786c28acf0b5fe31c8dca4c2add95098c709b11e8c35b424ebd4f5b",
+ "sha256:e883b61b75ca6efc2541fcd52a5c8ccfe288b24d97e20ac08fdf343b8ac672ea",
+ "sha256:f0a44bb40b6aaa4fb9a5c1ee07880570ecda2065433a96ccff409c9c20c1624a",
+ "sha256:f82ace0ec57c94aaf5b0e118d4366cff5889097412c75aa14b4fd5fc0c44ee3e",
+ "sha256:f9ca09414003c0e96a735daa1f071f7d7ed06962ef4fa29ceb6c80d06696d900",
+ "sha256:fa430b871220dc62572cef9c69b41e0d70fcb9d486a4a207a5de4c1f25d82593",
+ "sha256:fc262c3df78c8ff6020c782d9ce02e4bcffe4900ad71c0ecdad59943cba54442",
+ "sha256:fcd546782d03181b0b1d20b43d612429a90a68779659ba8045114b867971ab71",
+ "sha256:fd4ceeae2fb8cabdd1b71c82bfdd39662473d3433ec95b962200e9e752fb70d0",
+ "sha256:fec5fac7aea6c060f317f07494961236434928e6f4374e170ef50b3001e14581"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==3.10.9"
},
"aiosignal": {
"hashes": [
@@ -145,6 +145,15 @@
"markers": "python_version >= '3.9'",
"version": "==4.6.0"
},
+ "asgi-correlation-id": {
+ "hashes": [
+ "sha256:25d89b52f3d32c0f3b4915a9fc38d9cffc7395960d05910bdce5c13023dc237b",
+ "sha256:62ba38c359aa004c1c3e2b8e0cdf0e8ad4aa5a93864eaadc46e77d5c142a206a"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8' and python_version < '4.0'",
+ "version": "==4.3.3"
+ },
"attrs": {
"hashes": [
"sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346",
@@ -503,11 +512,11 @@
},
"dnspython": {
"hashes": [
- "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50",
- "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"
+ "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86",
+ "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"
],
- "markers": "python_version >= '3.8'",
- "version": "==2.6.1"
+ "markers": "python_version >= '3.9'",
+ "version": "==2.7.0"
},
"email-validator": {
"hashes": [
@@ -722,11 +731,11 @@
},
"httpcore": {
"hashes": [
- "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61",
- "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"
+ "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f",
+ "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"
],
"markers": "python_version >= '3.8'",
- "version": "==1.0.5"
+ "version": "==1.0.6"
},
"httptools": {
"hashes": [
@@ -949,69 +958,70 @@
},
"markupsafe": {
"hashes": [
- "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf",
- "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff",
- "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f",
- "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3",
- "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532",
- "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f",
- "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617",
- "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df",
- "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4",
- "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906",
- "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f",
- "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4",
- "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8",
- "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371",
- "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2",
- "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465",
- "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52",
- "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6",
- "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169",
- "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad",
- "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2",
- "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0",
- "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029",
- "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f",
- "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a",
- "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced",
- "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5",
- "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c",
- "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf",
- "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9",
- "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb",
- "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad",
- "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3",
- "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1",
- "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46",
- "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc",
- "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a",
- "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee",
- "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900",
- "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5",
- "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea",
- "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f",
- "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5",
- "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e",
- "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a",
- "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f",
- "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50",
- "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a",
- "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b",
- "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4",
- "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff",
- "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2",
- "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46",
- "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b",
- "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf",
- "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5",
- "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5",
- "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab",
- "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd",
- "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"
+ "sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396",
+ "sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38",
+ "sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a",
+ "sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8",
+ "sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b",
+ "sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad",
+ "sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a",
+ "sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a",
+ "sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da",
+ "sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6",
+ "sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8",
+ "sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344",
+ "sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a",
+ "sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8",
+ "sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5",
+ "sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7",
+ "sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170",
+ "sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132",
+ "sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9",
+ "sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd",
+ "sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9",
+ "sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346",
+ "sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc",
+ "sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589",
+ "sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5",
+ "sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915",
+ "sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295",
+ "sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453",
+ "sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea",
+ "sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b",
+ "sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d",
+ "sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b",
+ "sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4",
+ "sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b",
+ "sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7",
+ "sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf",
+ "sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f",
+ "sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91",
+ "sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd",
+ "sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50",
+ "sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b",
+ "sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583",
+ "sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a",
+ "sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984",
+ "sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c",
+ "sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c",
+ "sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25",
+ "sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa",
+ "sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4",
+ "sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3",
+ "sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97",
+ "sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1",
+ "sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd",
+ "sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772",
+ "sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a",
+ "sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729",
+ "sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca",
+ "sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6",
+ "sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635",
+ "sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b",
+ "sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f"
],
- "markers": "python_version >= '3.7'",
- "version": "==2.1.5"
+ "markers": "python_version >= '3.9'",
+ "version": "==3.0.1"
},
"marshmallow": {
"hashes": [
@@ -1423,6 +1433,110 @@
"markers": "python_version >= '3.8'",
"version": "==10.4.0"
},
+ "propcache": {
+ "hashes": [
+ "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9",
+ "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763",
+ "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325",
+ "sha256:140fbf08ab3588b3468932974a9331aff43c0ab8a2ec2c608b6d7d1756dbb6cb",
+ "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b",
+ "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09",
+ "sha256:1ec43d76b9677637a89d6ab86e1fef70d739217fefa208c65352ecf0282be957",
+ "sha256:20a617c776f520c3875cf4511e0d1db847a076d720714ae35ffe0df3e440be68",
+ "sha256:218db2a3c297a3768c11a34812e63b3ac1c3234c3a086def9c0fee50d35add1f",
+ "sha256:22aa8f2272d81d9317ff5756bb108021a056805ce63dd3630e27d042c8092798",
+ "sha256:25a1f88b471b3bc911d18b935ecb7115dff3a192b6fef46f0bfaf71ff4f12418",
+ "sha256:25c8d773a62ce0451b020c7b29a35cfbc05de8b291163a7a0f3b7904f27253e6",
+ "sha256:2a60ad3e2553a74168d275a0ef35e8c0a965448ffbc3b300ab3a5bb9956c2162",
+ "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f",
+ "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036",
+ "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8",
+ "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2",
+ "sha256:33ac8f098df0585c0b53009f039dfd913b38c1d2edafed0cedcc0c32a05aa110",
+ "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23",
+ "sha256:363ea8cd3c5cb6679f1c2f5f1f9669587361c062e4899fce56758efa928728f8",
+ "sha256:375a12d7556d462dc64d70475a9ee5982465fbb3d2b364f16b86ba9135793638",
+ "sha256:388f3217649d6d59292b722d940d4d2e1e6a7003259eb835724092a1cca0203a",
+ "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44",
+ "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2",
+ "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2",
+ "sha256:3dfafb44f7bb35c0c06eda6b2ab4bfd58f02729e7c4045e179f9a861b07c9850",
+ "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136",
+ "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b",
+ "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887",
+ "sha256:4569158070180c3855e9c0791c56be3ceeb192defa2cdf6a3f39e54319e56b89",
+ "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87",
+ "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348",
+ "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4",
+ "sha256:53d1bd3f979ed529f0805dd35ddaca330f80a9a6d90bc0121d2ff398f8ed8861",
+ "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e",
+ "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c",
+ "sha256:56bb5c98f058a41bb58eead194b4db8c05b088c93d94d5161728515bd52b052b",
+ "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb",
+ "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1",
+ "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de",
+ "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354",
+ "sha256:662dd62358bdeaca0aee5761de8727cfd6861432e3bb828dc2a693aa0471a563",
+ "sha256:676135dcf3262c9c5081cc8f19ad55c8a64e3f7282a21266d05544450bffc3a5",
+ "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf",
+ "sha256:67b69535c870670c9f9b14a75d28baa32221d06f6b6fa6f77a0a13c5a7b0a5b9",
+ "sha256:682a7c79a2fbf40f5dbb1eb6bfe2cd865376deeac65acf9beb607505dced9e12",
+ "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4",
+ "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5",
+ "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71",
+ "sha256:73e4b40ea0eda421b115248d7e79b59214411109a5bc47d0d48e4c73e3b8fcf9",
+ "sha256:74acd6e291f885678631b7ebc85d2d4aec458dd849b8c841b57ef04047833bed",
+ "sha256:7665f04d0c7f26ff8bb534e1c65068409bf4687aa2534faf7104d7182debb336",
+ "sha256:7735e82e3498c27bcb2d17cb65d62c14f1100b71723b68362872bca7d0913d90",
+ "sha256:77a86c261679ea5f3896ec060be9dc8e365788248cc1e049632a1be682442063",
+ "sha256:7cf18abf9764746b9c8704774d8b06714bcb0a63641518a3a89c7f85cc02c2ad",
+ "sha256:83928404adf8fb3d26793665633ea79b7361efa0287dfbd372a7e74311d51ee6",
+ "sha256:8e40876731f99b6f3c897b66b803c9e1c07a989b366c6b5b475fafd1f7ba3fb8",
+ "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e",
+ "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2",
+ "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7",
+ "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d",
+ "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d",
+ "sha256:9517d5e9e0731957468c29dbfd0f976736a0e55afaea843726e887f36fe017df",
+ "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b",
+ "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178",
+ "sha256:97e48e8875e6c13909c800fa344cd54cc4b2b0db1d5f911f840458a500fde2c2",
+ "sha256:9e0f07b42d2a50c7dd2d8675d50f7343d998c64008f1da5fef888396b7f84630",
+ "sha256:a3dc1a4b165283bd865e8f8cb5f0c64c05001e0718ed06250d8cac9bec115b48",
+ "sha256:a3ebe9a75be7ab0b7da2464a77bb27febcb4fab46a34f9288f39d74833db7f61",
+ "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89",
+ "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb",
+ "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3",
+ "sha256:b33d7a286c0dc1a15f5fc864cc48ae92a846df287ceac2dd499926c3801054a6",
+ "sha256:bc092ba439d91df90aea38168e11f75c655880c12782facf5cf9c00f3d42b562",
+ "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b",
+ "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58",
+ "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db",
+ "sha256:cfac69017ef97db2438efb854edf24f5a29fd09a536ff3a992b75990720cdc99",
+ "sha256:d2f0d0f976985f85dfb5f3d685697ef769faa6b71993b46b295cdbbd6be8cc37",
+ "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83",
+ "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a",
+ "sha256:d9b6ddac6408194e934002a69bcaadbc88c10b5f38fb9307779d1c629181815d",
+ "sha256:db47514ffdbd91ccdc7e6f8407aac4ee94cc871b15b577c1c324236b013ddd04",
+ "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70",
+ "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544",
+ "sha256:e63e3e1e0271f374ed489ff5ee73d4b6e7c60710e1f76af5f0e1a6117cd26394",
+ "sha256:e70fac33e8b4ac63dfc4c956fd7d85a0b1139adcfc0d964ce288b7c527537fea",
+ "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7",
+ "sha256:f45eec587dafd4b2d41ac189c2156461ebd0c1082d2fe7013571598abb8505d1",
+ "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793",
+ "sha256:f571aea50ba5623c308aa146eb650eebf7dbe0fd8c5d946e28343cb3b5aad577",
+ "sha256:f60f0ac7005b9f5a6091009b09a419ace1610e163fa5deaba5ce3484341840e7",
+ "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57",
+ "sha256:f6d5749fdd33d90e34c2efb174c7e236829147a2713334d708746e94c4bde40d",
+ "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032",
+ "sha256:fa1076244f54bb76e65e22cb6910365779d5c3d71d1f18b275f1dfc7b0d71b4d",
+ "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016",
+ "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==0.2.0"
+ },
"protobuf": {
"hashes": [
"sha256:2c69461a7fcc8e24be697624c09a839976d82ae75062b11a0972e41fd2cd9132",
@@ -1595,22 +1709,13 @@
"markers": "python_version >= '3.8'",
"version": "==1.0.1"
},
- "python-json-logger": {
- "hashes": [
- "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c",
- "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd"
- ],
- "index": "pypi",
- "markers": "python_version >= '3.6'",
- "version": "==2.0.7"
- },
"python-multipart": {
"hashes": [
- "sha256:2b06ad9e8d50c7a8db80e3b56dab590137b323410605af2be20d62a5f1ba1dc8",
- "sha256:46eb3c6ce6fdda5fb1a03c7e11d490e407c6930a2703fe7aef4da71c374688fa"
+ "sha256:045e1f98d719c1ce085ed7f7e1ef9d8ccc8c02ba02b5566d5f7521410ced58cb",
+ "sha256:43dcf96cf65888a9cd3423544dd0d75ac10f7aa0c3c28a175bbcd00c9ce1aebf"
],
"markers": "python_version >= '3.8'",
- "version": "==0.0.10"
+ "version": "==0.0.12"
},
"pytz": {
"hashes": [
@@ -1788,11 +1893,11 @@
},
"rich": {
"hashes": [
- "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06",
- "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"
+ "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c",
+ "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1"
],
- "markers": "python_full_version >= '3.7.0'",
- "version": "==13.8.1"
+ "markers": "python_full_version >= '3.8.0'",
+ "version": "==13.9.2"
},
"setuptools": {
"hashes": [
@@ -1907,6 +2012,15 @@
],
"version": "==0.0.26"
},
+ "structlog": {
+ "hashes": [
+ "sha256:597f61e80a91cc0749a9fd2a098ed76715a1c8a01f73e336b746504d1aad7610",
+ "sha256:b27bfecede327a6d2da5fbc96bd859f114ecc398a6389d664f62085ee7ae6fc4"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==24.4.0"
+ },
"tenacity": {
"hashes": [
"sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78",
@@ -1917,45 +2031,40 @@
},
"tiktoken": {
"hashes": [
- "sha256:03c6c40ff1db0f48a7b4d2dafeae73a5607aacb472fa11f125e7baf9dce73704",
- "sha256:084cec29713bc9d4189a937f8a35dbdfa785bd1235a34c1124fe2323821ee93f",
- "sha256:09ed925bccaa8043e34c519fbb2f99110bd07c6fd67714793c21ac298e449410",
- "sha256:0bc603c30b9e371e7c4c7935aba02af5994a909fc3c0fe66e7004070858d3f8f",
- "sha256:1063c5748be36344c7e18c7913c53e2cca116764c2080177e57d62c7ad4576d1",
- "sha256:1077266e949c24e0291f6c350433c6f0971365ece2b173a23bc3b9f9defef6b6",
- "sha256:10c7674f81e6e350fcbed7c09a65bca9356eaab27fb2dac65a1e440f2bcfe30f",
- "sha256:131b8aeb043a8f112aad9f46011dced25d62629091e51d9dc1adbf4a1cc6aa98",
- "sha256:13c94efacdd3de9aff824a788353aa5749c0faee1fbe3816df365ea450b82311",
- "sha256:20295d21419bfcca092644f7e2f2138ff947a6eb8cfc732c09cc7d76988d4a89",
- "sha256:21a20c3bd1dd3e55b91c1331bf25f4af522c525e771691adbc9a69336fa7f702",
- "sha256:2398fecd38c921bcd68418675a6d155fad5f5e14c2e92fcf5fe566fa5485a858",
- "sha256:2bcb28ddf79ffa424f171dfeef9a4daff61a94c631ca6813f43967cb263b83b9",
- "sha256:2ee92776fdbb3efa02a83f968c19d4997a55c8e9ce7be821ceee04a1d1ee149c",
- "sha256:485f3cc6aba7c6b6ce388ba634fbba656d9ee27f766216f45146beb4ac18b25f",
- "sha256:54031f95c6939f6b78122c0aa03a93273a96365103793a22e1793ee86da31685",
- "sha256:5d4511c52caacf3c4981d1ae2df85908bd31853f33d30b345c8b6830763f769c",
- "sha256:71c55d066388c55a9c00f61d2c456a6086673ab7dec22dd739c23f77195b1908",
- "sha256:79383a6e2c654c6040e5f8506f3750db9ddd71b550c724e673203b4f6b4b4590",
- "sha256:811229fde1652fedcca7c6dfe76724d0908775b353556d8a71ed74d866f73f7b",
- "sha256:861f9ee616766d736be4147abac500732b505bf7013cfaf019b85892637f235e",
- "sha256:86b6e7dc2e7ad1b3757e8a24597415bafcfb454cebf9a33a01f2e6ba2e663992",
- "sha256:8a81bac94769cab437dd3ab0b8a4bc4e0f9cf6835bcaa88de71f39af1791727a",
- "sha256:8c46d7af7b8c6987fac9b9f61041b452afe92eb087d29c9ce54951280f899a97",
- "sha256:8d57f29171255f74c0aeacd0651e29aa47dff6f070cb9f35ebc14c82278f3b25",
- "sha256:8e58c7eb29d2ab35a7a8929cbeea60216a4ccdf42efa8974d8e176d50c9a3df5",
- "sha256:8f5f6afb52fb8a7ea1c811e435e4188f2bef81b5e0f7a8635cc79b0eef0193d6",
- "sha256:959d993749b083acc57a317cbc643fb85c014d055b2119b739487288f4e5d1cb",
- "sha256:c72baaeaefa03ff9ba9688624143c858d1f6b755bb85d456d59e529e17234769",
- "sha256:cabc6dc77460df44ec5b879e68692c63551ae4fae7460dd4ff17181df75f1db7",
- "sha256:d20b5c6af30e621b4aca094ee61777a44118f52d886dbe4f02b70dfe05c15350",
- "sha256:d427614c3e074004efa2f2411e16c826f9df427d3c70a54725cae860f09e4bf4",
- "sha256:d6d73ea93e91d5ca771256dfc9d1d29f5a554b83821a1dc0891987636e0ae226",
- "sha256:e215292e99cb41fbc96988ef62ea63bb0ce1e15f2c147a61acc319f8b4cbe5bf",
- "sha256:e54be9a2cd2f6d6ffa3517b064983fb695c9a9d8aa7d574d1ef3c3f931a99225",
- "sha256:fffdcb319b614cf14f04d02a52e26b1d1ae14a570f90e9b55461a72672f7b13d"
+ "sha256:02be1666096aff7da6cbd7cdaa8e7917bfed3467cd64b38b1f112e96d3b06a24",
+ "sha256:1473cfe584252dc3fa62adceb5b1c763c1874e04511b197da4e6de51d6ce5a02",
+ "sha256:18228d624807d66c87acd8f25fc135665617cab220671eb65b50f5d70fa51f69",
+ "sha256:25e13f37bc4ef2d012731e93e0fef21dc3b7aea5bb9009618de9a4026844e560",
+ "sha256:294440d21a2a51e12d4238e68a5972095534fe9878be57d905c476017bff99fc",
+ "sha256:2efaf6199717b4485031b4d6edb94075e4d79177a172f38dd934d911b588d54a",
+ "sha256:326624128590def898775b722ccc327e90b073714227175ea8febbc920ac0a99",
+ "sha256:4177faa809bd55f699e88c96d9bb4635d22e3f59d635ba6fd9ffedf7150b9953",
+ "sha256:5376b6f8dc4753cd81ead935c5f518fa0fbe7e133d9e25f648d8c4dabdd4bad7",
+ "sha256:5637e425ce1fc49cf716d88df3092048359a4b3bbb7da762840426e937ada06d",
+ "sha256:56edfefe896c8f10aba372ab5706b9e3558e78db39dd497c940b47bf228bc419",
+ "sha256:6adc8323016d7758d6de7313527f755b0fc6c72985b7d9291be5d96d73ecd1e1",
+ "sha256:6b231f5e8982c245ee3065cd84a4712d64692348bc609d84467c57b4b72dcbc5",
+ "sha256:6b2ddbc79a22621ce8b1166afa9f9a888a664a579350dc7c09346a3b5de837d9",
+ "sha256:7e17807445f0cf1f25771c9d86496bd8b5c376f7419912519699f3cc4dc5c12e",
+ "sha256:845287b9798e476b4d762c3ebda5102be87ca26e5d2c9854002825d60cdb815d",
+ "sha256:881839cfeae051b3628d9823b2e56b5cc93a9e2efb435f4cf15f17dc45f21586",
+ "sha256:886f80bd339578bbdba6ed6d0567a0d5c6cfe198d9e587ba6c447654c65b8edc",
+ "sha256:9269348cb650726f44dd3bbb3f9110ac19a8dcc8f54949ad3ef652ca22a38e21",
+ "sha256:9a58deb7075d5b69237a3ff4bb51a726670419db6ea62bdcd8bd80c78497d7ab",
+ "sha256:9ccbb2740f24542534369c5635cfd9b2b3c2490754a78ac8831d99f89f94eeb2",
+ "sha256:9fb0e352d1dbe15aba082883058b3cce9e48d33101bdaac1eccf66424feb5b47",
+ "sha256:b07e33283463089c81ef1467180e3e00ab00d46c2c4bbcef0acab5f771d6695e",
+ "sha256:b591fb2b30d6a72121a80be24ec7a0e9eb51c5500ddc7e4c2496516dd5e3816b",
+ "sha256:c94ff53c5c74b535b2cbf431d907fc13c678bbd009ee633a2aca269a04389f9a",
+ "sha256:d2908c0d043a7d03ebd80347266b0e58440bdef5564f84f4d29fb235b5df3b04",
+ "sha256:d622d8011e6d6f239297efa42a2657043aaed06c4f68833550cac9e9bc723ef1",
+ "sha256:d8c2d0e5ba6453a290b86cd65fc51fedf247e1ba170191715b049dac1f628005",
+ "sha256:d8f3192733ac4d77977432947d563d7e1b310b96497acd3c196c9bddb36ed9db",
+ "sha256:f13d13c981511331eac0d01a59b5df7c0d4060a8be1e378672822213da51e0a2",
+ "sha256:fe9399bdc3f29d428f16a2f86c3c8ec20be3eac5f53693ce4980371c3245729b"
],
- "markers": "python_version >= '3.8'",
- "version": "==0.7.0"
+ "markers": "python_version >= '3.9'",
+ "version": "==0.8.0"
},
"tqdm": {
"hashes": [
@@ -2093,11 +2202,11 @@
"standard"
],
"hashes": [
- "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788",
- "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"
+ "sha256:13bc21373d103859f68fe739608e2eb054a816dea79189bc3ca08ea89a275906",
+ "sha256:cac7be4dd4d891c363cd942160a7b02e69150dcbc7a36be04d5f4af4b17c8ced"
],
"markers": "python_version >= '3.8'",
- "version": "==0.30.6"
+ "version": "==0.31.0"
},
"uvloop": {
"hashes": [
@@ -2400,101 +2509,101 @@
},
"yarl": {
"hashes": [
- "sha256:0103c52f8dfe5d573c856322149ddcd6d28f51b4d4a3ee5c4b3c1b0a05c3d034",
- "sha256:01549468858b87d36f967c97d02e6e54106f444aeb947ed76f8f71f85ed07cec",
- "sha256:0274b1b7a9c9c32b7bf250583e673ff99fb9fccb389215841e2652d9982de740",
- "sha256:0ac33d22b2604b020569a82d5f8a03ba637ba42cc1adf31f616af70baf81710b",
- "sha256:0d0a5e87bc48d76dfcfc16295201e9812d5f33d55b4a0b7cad1025b92bf8b91b",
- "sha256:10b690cd78cbaca2f96a7462f303fdd2b596d3978b49892e4b05a7567c591572",
- "sha256:126309c0f52a2219b3d1048aca00766429a1346596b186d51d9fa5d2070b7b13",
- "sha256:15871130439ad10abb25a4631120d60391aa762b85fcab971411e556247210a0",
- "sha256:17d4dc4ff47893a06737b8788ed2ba2f5ac4e8bb40281c8603920f7d011d5bdd",
- "sha256:18c2a7757561f05439c243f517dbbb174cadfae3a72dee4ae7c693f5b336570f",
- "sha256:1d4017e78fb22bc797c089b746230ad78ecd3cdb215bc0bd61cb72b5867da57e",
- "sha256:1f50a37aeeb5179d293465e522fd686080928c4d89e0ff215e1f963405ec4def",
- "sha256:20d817c0893191b2ab0ba30b45b77761e8dfec30a029b7c7063055ca71157f84",
- "sha256:22839d1d1eab9e4b427828a88a22beb86f67c14d8ff81175505f1cc8493f3500",
- "sha256:22dda2799c8d39041d731e02bf7690f0ef34f1691d9ac9dfcb98dd1e94c8b058",
- "sha256:2376d8cf506dffd0e5f2391025ae8675b09711016656590cb03b55894161fcfa",
- "sha256:24197ba3114cc85ddd4091e19b2ddc62650f2e4a899e51b074dfd52d56cf8c72",
- "sha256:24416bb5e221e29ddf8aac5b97e94e635ca2c5be44a1617ad6fe32556df44294",
- "sha256:2631c9d7386bd2d4ce24ecc6ebf9ae90b3efd713d588d90504eaa77fec4dba01",
- "sha256:28389a68981676bf74e2e199fe42f35d1aa27a9c98e3a03e6f58d2d3d054afe1",
- "sha256:2aee7594d2c2221c717a8e394bbed4740029df4c0211ceb0f04815686e99c795",
- "sha256:2e430ac432f969ef21770645743611c1618362309e3ad7cab45acd1ad1a540ff",
- "sha256:2e912b282466444023610e4498e3795c10e7cfd641744524876239fcf01d538d",
- "sha256:30ffc046ebddccb3c4cac72c1a3e1bc343492336f3ca86d24672e90ccc5e788a",
- "sha256:319c206e83e46ec2421b25b300c8482b6fe8a018baca246be308c736d9dab267",
- "sha256:326b8a079a9afcac0575971e56dabdf7abb2ea89a893e6949b77adfeb058b50e",
- "sha256:36ee0115b9edca904153a66bb74a9ff1ce38caff015de94eadfb9ba8e6ecd317",
- "sha256:3e26e64f42bce5ddf9002092b2c37b13071c2e6413d5c05f9fa9de58ed2f7749",
- "sha256:4ea99e64b2ad2635e0f0597b63f5ea6c374791ff2fa81cdd4bad8ed9f047f56f",
- "sha256:501a1576716032cc6d48c7c47bcdc42d682273415a8f2908e7e72cb4625801f3",
- "sha256:54c8cee662b5f8c30ad7eedfc26123f845f007798e4ff1001d9528fe959fd23c",
- "sha256:595bbcdbfc4a9c6989d7489dca8510cba053ff46b16c84ffd95ac8e90711d419",
- "sha256:5b860055199aec8d6fe4dcee3c5196ce506ca198a50aab0059ffd26e8e815828",
- "sha256:5c667b383529520b8dd6bd496fc318678320cb2a6062fdfe6d3618da6b8790f6",
- "sha256:5fb475a4cdde582c9528bb412b98f899680492daaba318231e96f1a0a1bb0d53",
- "sha256:607d12f0901f6419a8adceb139847c42c83864b85371f58270e42753f9780fa6",
- "sha256:64c5b0f2b937fe40d0967516eee5504b23cb247b8b7ffeba7213a467d9646fdc",
- "sha256:664380c7ed524a280b6a2d5d9126389c3e96cd6e88986cdb42ca72baa27421d6",
- "sha256:6af871f70cfd5b528bd322c65793b5fd5659858cdfaa35fbe563fb99b667ed1f",
- "sha256:6c89894cc6f6ddd993813e79244b36b215c14f65f9e4f1660b1f2ba9e5594b95",
- "sha256:6dee0496d5f1a8f57f0f28a16f81a2033fc057a2cf9cd710742d11828f8c80e2",
- "sha256:6e9a9f50892153bad5046c2a6df153224aa6f0573a5a8ab44fc54a1e886f6e21",
- "sha256:712ba8722c0699daf186de089ddc4677651eb9875ed7447b2ad50697522cbdd9",
- "sha256:717f185086bb9d817d4537dd18d5df5d657598cd00e6fc22e4d54d84de266c1d",
- "sha256:71978ba778948760cff528235c951ea0ef7a4f9c84ac5a49975f8540f76c3f73",
- "sha256:71af3766bb46738d12cc288d9b8de7ef6f79c31fd62757e2b8a505fe3680b27f",
- "sha256:73a183042ae0918c82ce2df38c3db2409b0eeae88e3afdfc80fb67471a95b33b",
- "sha256:7564525a4673fde53dee7d4c307a961c0951918f0b8c7f09b2c9e02067cf6504",
- "sha256:76a59d1b63de859398bc7764c860a769499511463c1232155061fe0147f13e01",
- "sha256:7e9905fc2dc1319e4c39837b906a024cf71b1261cc66b0cd89678f779c0c61f5",
- "sha256:8112f640a4f7e7bf59f7cabf0d47a29b8977528c521d73a64d5cc9e99e48a174",
- "sha256:835010cc17d0020e7931d39e487d72c8e01c98e669b6896a8b8c9aa8ca69a949",
- "sha256:838dde2cb570cfbb4cab8a876a0974e8b90973ea40b3ac27a79b8a74c8a2db15",
- "sha256:8d31dd0245d88cf7239e96e8f2a99f815b06e458a5854150f8e6f0e61618d41b",
- "sha256:96b34830bd6825ca0220bf005ea99ac83eb9ce51301ddb882dcf613ae6cd95fb",
- "sha256:96c8ff1e1dd680e38af0887927cab407a4e51d84a5f02ae3d6eb87233036c763",
- "sha256:9a7ee79183f0b17dcede8b6723e7da2ded529cf159a878214be9a5d3098f5b1e",
- "sha256:a3e2aff8b822ab0e0bdbed9f50494b3a35629c4b9488ae391659973a37a9f53f",
- "sha256:a4f3ab9eb8ab2d585ece959c48d234f7b39ac0ca1954a34d8b8e58a52064bdb3",
- "sha256:a8b54949267bd5704324397efe9fbb6aa306466dee067550964e994d309db5f1",
- "sha256:a96198d5d26f40557d986c1253bfe0e02d18c9d9b93cf389daf1a3c9f7c755fa",
- "sha256:aebbd47df77190ada603157f0b3670d578c110c31746ecc5875c394fdcc59a99",
- "sha256:af1107299cef049ad00a93df4809517be432283a0847bcae48343ebe5ea340dc",
- "sha256:b63465b53baeaf2122a337d4ab57d6bbdd09fcadceb17a974cfa8a0300ad9c67",
- "sha256:ba1c779b45a399cc25f511c681016626f69e51e45b9d350d7581998722825af9",
- "sha256:bce00f3b1f7f644faae89677ca68645ed5365f1c7f874fdd5ebf730a69640d38",
- "sha256:bfdf419bf5d3644f94cd7052954fc233522f5a1b371fc0b00219ebd9c14d5798",
- "sha256:c1caa5763d1770216596e0a71b5567f27aac28c95992110212c108ec74589a48",
- "sha256:c3e4e1f7b08d1ec6b685ccd3e2d762219c550164fbf524498532e39f9413436e",
- "sha256:c85ab016e96a975afbdb9d49ca90f3bca9920ef27c64300843fe91c3d59d8d20",
- "sha256:c924deab8105f86980983eced740433fb7554a7f66db73991affa4eda99d5402",
- "sha256:d4f818f6371970d6a5d1e42878389bbfb69dcde631e4bbac5ec1cb11158565ca",
- "sha256:d920401941cb898ef089422e889759dd403309eb370d0e54f1bdf6ca07fef603",
- "sha256:da045bd1147d12bd43fb032296640a7cc17a7f2eaba67495988362e99db24fd2",
- "sha256:dc3192a81ecd5ff954cecd690327badd5a84d00b877e1573f7c9097ce13e5bfb",
- "sha256:ddae504cfb556fe220efae65e35be63cd11e3c314b202723fc2119ce19f0ca2e",
- "sha256:de4544b1fb29cf14870c4e2b8a897c0242449f5dcebd3e0366aa0aa3cf58a23a",
- "sha256:dea360778e0668a7ad25d7727d03364de8a45bfd5d808f81253516b9f2217765",
- "sha256:e2254fe137c4a360b0a13173a56444f756252c9283ba4d267ca8e9081cd140ea",
- "sha256:e64f0421892a207d3780903085c1b04efeb53b16803b23d947de5a7261b71355",
- "sha256:e97a29b37830ba1262d8dfd48ddb5b28ad4d3ebecc5d93a9c7591d98641ec737",
- "sha256:eacbcf30efaca7dc5cb264228ffecdb95fdb1e715b1ec937c0ce6b734161e0c8",
- "sha256:eee5ff934b0c9f4537ff9596169d56cab1890918004791a7a06b879b3ba2a7ef",
- "sha256:eff6bac402719c14e17efe845d6b98593c56c843aca6def72080fbede755fd1f",
- "sha256:f10954b233d4df5cc3137ffa5ced97f8894152df817e5d149bf05a0ef2ab8134",
- "sha256:f23bb1a7a6e8e8b612a164fdd08e683bcc16c76f928d6dbb7bdbee2374fbfee6",
- "sha256:f494c01b28645c431239863cb17af8b8d15b93b0d697a0320d5dd34cd9d7c2fa",
- "sha256:f6a071d2c3d39b4104f94fc08ab349e9b19b951ad4b8e3b6d7ea92d6ef7ccaf8",
- "sha256:f736f54565f8dd7e3ab664fef2bc461d7593a389a7f28d4904af8d55a91bd55f",
- "sha256:f8981a94a27ac520a398302afb74ae2c0be1c3d2d215c75c582186a006c9e7b0",
- "sha256:fd24996e12e1ba7c397c44be75ca299da14cde34d74bc5508cce233676cc68d0",
- "sha256:ff54340fc1129e8e181827e2234af3ff659b4f17d9bbe77f43bc19e6577fadec"
- ],
- "markers": "python_version >= '3.8'",
- "version": "==1.12.1"
+ "sha256:047b258e00b99091b6f90355521f026238c63bd76dcf996d93527bb13320eefd",
+ "sha256:06ff23462398333c78b6f4f8d3d70410d657a471c2c5bbe6086133be43fc8f1a",
+ "sha256:07f9eaf57719d6721ab15805d85f4b01a5b509a0868d7320134371bcb652152d",
+ "sha256:0aa92e3e30a04f9462a25077db689c4ac5ea9ab6cc68a2e563881b987d42f16d",
+ "sha256:0cf21f46a15d445417de8fc89f2568852cf57fe8ca1ab3d19ddb24d45c0383ae",
+ "sha256:0fd7b941dd1b00b5f0acb97455fea2c4b7aac2dd31ea43fb9d155e9bc7b78664",
+ "sha256:147e36331f6f63e08a14640acf12369e041e0751bb70d9362df68c2d9dcf0c87",
+ "sha256:16a682a127930f3fc4e42583becca6049e1d7214bcad23520c590edd741d2114",
+ "sha256:176110bff341b6730f64a1eb3a7070e12b373cf1c910a9337e7c3240497db76f",
+ "sha256:19268b4fec1d7760134f2de46ef2608c2920134fb1fa61e451f679e41356dc55",
+ "sha256:1b16f6c75cffc2dc0616ea295abb0e1967601bd1fb1e0af6a1de1c6c887f3439",
+ "sha256:1bfc25aa6a7c99cf86564210f79a0b7d4484159c67e01232b116e445b3036547",
+ "sha256:1ca3894e9e9f72da93544f64988d9c052254a338a9f855165f37f51edb6591de",
+ "sha256:1dda53508df0de87b6e6b0a52d6718ff6c62a5aca8f5552748404963df639269",
+ "sha256:217a782020b875538eebf3948fac3a7f9bbbd0fd9bf8538f7c2ad7489e80f4e8",
+ "sha256:2192f718db4a8509f63dd6d950f143279211fa7e6a2c612edc17d85bf043d36e",
+ "sha256:29a84a46ec3ebae7a1c024c055612b11e9363a8a23238b3e905552d77a2bc51b",
+ "sha256:3007a5b75cb50140708420fe688c393e71139324df599434633019314ceb8b59",
+ "sha256:30600ba5db60f7c0820ef38a2568bb7379e1418ecc947a0f76fd8b2ff4257a97",
+ "sha256:337912bcdcf193ade64b9aae5a4017a0a1950caf8ca140362e361543c6773f21",
+ "sha256:37001e5d4621cef710c8dc1429ca04e189e572f128ab12312eab4e04cf007132",
+ "sha256:3d569f877ed9a708e4c71a2d13d2940cb0791da309f70bd970ac1a5c088a0a92",
+ "sha256:4009def9be3a7e5175db20aa2d7307ecd00bbf50f7f0f989300710eee1d0b0b9",
+ "sha256:46a9772a1efa93f9cd170ad33101c1817c77e0e9914d4fe33e2da299d7cf0f9b",
+ "sha256:47eede5d11d669ab3759b63afb70d28d5328c14744b8edba3323e27dc52d298d",
+ "sha256:498b3c55087b9d762636bca9b45f60d37e51d24341786dc01b81253f9552a607",
+ "sha256:4e0d45ebf975634468682c8bec021618b3ad52c37619e5c938f8f831fa1ac5c0",
+ "sha256:4f24f08b6c9b9818fd80612c97857d28f9779f0d1211653ece9844fc7b414df2",
+ "sha256:55c144d363ad4626ca744556c049c94e2b95096041ac87098bb363dcc8635e8d",
+ "sha256:582cedde49603f139be572252a318b30dc41039bc0b8165f070f279e5d12187f",
+ "sha256:587c3cc59bc148a9b1c07a019346eda2549bc9f468acd2f9824d185749acf0a6",
+ "sha256:5cd5dad8366e0168e0fd23d10705a603790484a6dbb9eb272b33673b8f2cce72",
+ "sha256:5d02d700705d67e09e1f57681f758f0b9d4412eeb70b2eb8d96ca6200b486db3",
+ "sha256:625f207b1799e95e7c823f42f473c1e9dbfb6192bd56bba8695656d92be4535f",
+ "sha256:659603d26d40dd4463200df9bfbc339fbfaed3fe32e5c432fe1dc2b5d4aa94b4",
+ "sha256:689a99a42ee4583fcb0d3a67a0204664aa1539684aed72bdafcbd505197a91c4",
+ "sha256:68ac1a09392ed6e3fd14be880d39b951d7b981fd135416db7d18a6208c536561",
+ "sha256:6a615cad11ec3428020fb3c5a88d85ce1b5c69fd66e9fcb91a7daa5e855325dd",
+ "sha256:73bedd2be05f48af19f0f2e9e1353921ce0c83f4a1c9e8556ecdcf1f1eae4892",
+ "sha256:742aef0a99844faaac200564ea6f5e08facb285d37ea18bd1a5acf2771f3255a",
+ "sha256:75ff4c819757f9bdb35de049a509814d6ce851fe26f06eb95a392a5640052482",
+ "sha256:781e2495e408a81e4eaeedeb41ba32b63b1980dddf8b60dbbeff6036bcd35049",
+ "sha256:7a9f917966d27f7ce30039fe8d900f913c5304134096554fd9bea0774bcda6d1",
+ "sha256:7e2637d75e92763d1322cb5041573279ec43a80c0f7fbbd2d64f5aee98447b17",
+ "sha256:8089d4634d8fa2b1806ce44fefa4979b1ab2c12c0bc7ef3dfa45c8a374811348",
+ "sha256:816d24f584edefcc5ca63428f0b38fee00b39fe64e3c5e558f895a18983efe96",
+ "sha256:8385ab36bf812e9d37cf7613999a87715f27ef67a53f0687d28c44b819df7cb0",
+ "sha256:85cb3e40eaa98489f1e2e8b29f5ad02ee1ee40d6ce6b88d50cf0f205de1d9d2c",
+ "sha256:8648180b34faaea4aa5b5ca7e871d9eb1277033fa439693855cf0ea9195f85f1",
+ "sha256:8892fa575ac9b1b25fae7b221bc4792a273877b9b56a99ee2d8d03eeb3dbb1d2",
+ "sha256:88c7d9d58aab0724b979ab5617330acb1c7030b79379c8138c1c8c94e121d1b3",
+ "sha256:8a2f8fb7f944bcdfecd4e8d855f84c703804a594da5123dd206f75036e536d4d",
+ "sha256:8f4e475f29a9122f908d0f1f706e1f2fc3656536ffd21014ff8a6f2e1b14d1d8",
+ "sha256:8f50eb3837012a937a2b649ec872b66ba9541ad9d6f103ddcafb8231cfcafd22",
+ "sha256:91d875f75fabf76b3018c5f196bf3d308ed2b49ddcb46c1576d6b075754a1393",
+ "sha256:94b2bb9bcfd5be9d27004ea4398fb640373dd0c1a9e219084f42c08f77a720ab",
+ "sha256:9557c9322aaa33174d285b0c1961fb32499d65ad1866155b7845edc876c3c835",
+ "sha256:95e16e9eaa2d7f5d87421b8fe694dd71606aa61d74b824c8d17fc85cc51983d1",
+ "sha256:96952f642ac69075e44c7d0284528938fdff39422a1d90d3e45ce40b72e5e2d9",
+ "sha256:985623575e5c4ea763056ffe0e2d63836f771a8c294b3de06d09480538316b13",
+ "sha256:99ff3744f5fe48288be6bc402533b38e89749623a43208e1d57091fc96b783b9",
+ "sha256:9abe80ae2c9d37c17599557b712e6515f4100a80efb2cda15f5f070306477cd2",
+ "sha256:a152751af7ef7b5d5fa6d215756e508dd05eb07d0cf2ba51f3e740076aa74373",
+ "sha256:a2e4725a08cb2b4794db09e350c86dee18202bb8286527210e13a1514dc9a59a",
+ "sha256:a56fbe3d7f3bce1d060ea18d2413a2ca9ca814eea7cedc4d247b5f338d54844e",
+ "sha256:ab3abc0b78a5dfaa4795a6afbe7b282b6aa88d81cf8c1bb5e394993d7cae3457",
+ "sha256:b03384eed107dbeb5f625a99dc3a7de8be04fc8480c9ad42fccbc73434170b20",
+ "sha256:b0547ab1e9345dc468cac8368d88ea4c5bd473ebc1d8d755347d7401982b5dd8",
+ "sha256:b4c1ecba93e7826dc71ddba75fb7740cdb52e7bd0be9f03136b83f54e6a1f511",
+ "sha256:b693c63e7e64b524f54aa4888403c680342d1ad0d97be1707c531584d6aeeb4f",
+ "sha256:b6d0147574ce2e7b812c989e50fa72bbc5338045411a836bd066ce5fc8ac0bce",
+ "sha256:b9cfef3f14f75bf6aba73a76caf61f9d00865912a04a4393c468a7ce0981b519",
+ "sha256:b9f805e37ed16cc212fdc538a608422d7517e7faf539bedea4fe69425bc55d76",
+ "sha256:bab03192091681d54e8225c53f270b0517637915d9297028409a2a5114ff4634",
+ "sha256:bc24f968b82455f336b79bf37dbb243b7d76cd40897489888d663d4e028f5069",
+ "sha256:c14b504a74e58e2deb0378b3eca10f3d076635c100f45b113c18c770b4a47a50",
+ "sha256:c2089a9afef887664115f7fa6d3c0edd6454adaca5488dba836ca91f60401075",
+ "sha256:c8ed4034f0765f8861620c1f2f2364d2e58520ea288497084dae880424fc0d9f",
+ "sha256:cd2660c01367eb3ef081b8fa0a5da7fe767f9427aa82023a961a5f28f0d4af6c",
+ "sha256:d8361c7d04e6a264481f0b802e395f647cd3f8bbe27acfa7c12049efea675bd1",
+ "sha256:d9baec588f015d0ee564057aa7574313c53a530662ffad930b7886becc85abdf",
+ "sha256:dbd9ff43a04f8ffe8a959a944c2dca10d22f5f99fc6a459f49c3ebfb409309d9",
+ "sha256:e3f8bfc1db82589ef965ed234b87de30d140db8b6dc50ada9e33951ccd8ec07a",
+ "sha256:e6a2c5c5bb2556dfbfffffc2bcfb9c235fd2b566d5006dfb2a37afc7e3278a07",
+ "sha256:e749af6c912a7bb441d105c50c1a3da720474e8acb91c89350080dd600228f0e",
+ "sha256:e85d86527baebb41a214cc3b45c17177177d900a2ad5783dbe6f291642d4906f",
+ "sha256:ee2c68e4f2dd1b1c15b849ba1c96fac105fca6ffdb7c1e8be51da6fabbdeafb9",
+ "sha256:f3ab950f8814f3b7b5e3eebc117986f817ec933676f68f0a6c5b2137dd7c9c69",
+ "sha256:f4f4547944d4f5cfcdc03f3f097d6f05bbbc915eaaf80a2ee120d0e756de377d",
+ "sha256:f72a0d746d38cb299b79ce3d4d60ba0892c84bbc905d0d49c13df5bace1b65f8",
+ "sha256:fc2c80bc87fba076e6cbb926216c27fba274dae7100a7b9a0983b53132dd99f2",
+ "sha256:fe4d2536c827f508348d7b40c08767e8c7071614250927233bf0c92170451c0a"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.14.0"
},
"zipp": {
"hashes": [
@@ -2563,11 +2672,11 @@
},
"botocore-stubs": {
"hashes": [
- "sha256:1184dcb19d833041cfadc3c533c8cb7ae246a9ab9b974b33b42fe209f54f551b",
- "sha256:acb1c77d422c9bf51161c98a2912fd0b4abb4efe9578d7f4c851c77d30fc754a"
+ "sha256:b1aebecdfa4f4fc02b0a68a5e438877034b195168809a7202ee32b42245d3ece",
+ "sha256:d79a408dfc503a1a0389d10cd29ad22a01450d0d53902ea216815e2ba98913ba"
],
"markers": "python_version >= '3.8'",
- "version": "==1.35.26"
+ "version": "==1.35.35"
},
"certifi": {
"hashes": [
@@ -2952,11 +3061,11 @@
},
"rich": {
"hashes": [
- "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06",
- "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"
+ "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c",
+ "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1"
],
- "markers": "python_full_version >= '3.7.0'",
- "version": "==13.8.1"
+ "markers": "python_full_version >= '3.8.0'",
+ "version": "==13.9.2"
},
"ruff": {
"hashes": [
@@ -3000,11 +3109,11 @@
},
"types-awscrt": {
"hashes": [
- "sha256:117ff2b1bb657f09d01b7e0ce3fe3fa6e039be12d30b826896182725c9ce85b1",
- "sha256:9f7f47de68799cb2bcb9e486f48d77b9f58962b92fba43cb8860da70b3c57d1b"
+ "sha256:67a660c90bad360c339f6a79310cc17094d12472042c7ca5a41450aaf5fc9a54",
+ "sha256:b2c196bbd3226bab42d80fae13c34548de9ddc195f5a366d79c15d18e5897aa9"
],
"markers": "python_version >= '3.8'",
- "version": "==0.21.5"
+ "version": "==0.22.0"
},
"types-beautifulsoup4": {
"hashes": [
diff --git a/opentrons-ai-server/api/domain/openai_predict.py b/opentrons-ai-server/api/domain/openai_predict.py
index c9733614458..71b34cff12b 100644
--- a/opentrons-ai-server/api/domain/openai_predict.py
+++ b/opentrons-ai-server/api/domain/openai_predict.py
@@ -1,7 +1,8 @@
-import logging
from pathlib import Path
from typing import List, Tuple
+import structlog
+from ddtrace import tracer
from llama_index.core import Settings as li_settings
from llama_index.core import StorageContext, load_index_from_storage
from llama_index.embeddings.openai import OpenAIEmbedding
@@ -25,8 +26,8 @@
from api.domain.utils import refine_characters
from api.settings import Settings
-logger = logging.getLogger(__name__)
-
+settings: Settings = Settings()
+logger = structlog.stdlib.get_logger(settings.logger_name)
ROOT_PATH: Path = Path(Path(__file__)).parent.parent.parent
@@ -38,6 +39,7 @@ def __init__(self, settings: Settings) -> None:
model_name="text-embedding-3-large", api_key=self.settings.openai_api_key.get_secret_value()
)
+ @tracer.wrap()
def get_docs_all(self, query: str) -> Tuple[str, str, str]:
commands = self.extract_atomic_description(query)
logger.info("Commands", extra={"commands": commands})
@@ -85,6 +87,7 @@ def get_docs_all(self, query: str) -> Tuple[str, str, str]:
return example_commands, docs + docs_ref, standard_api_names
+ @tracer.wrap()
def extract_atomic_description(self, protocol_description: str) -> List[str]:
class atomic_descr(BaseModel):
"""
@@ -106,6 +109,7 @@ class atomic_descr(BaseModel):
descriptions.append(x)
return descriptions
+ @tracer.wrap()
def refine_response(self, assistant_message: str) -> str:
if assistant_message is None:
return ""
@@ -129,6 +133,7 @@ def refine_response(self, assistant_message: str) -> str:
return response.choices[0].message.content if response.choices[0].message.content is not None else ""
+ @tracer.wrap()
def predict(self, prompt: str, chat_completion_message_params: List[ChatCompletionMessageParam] | None = None) -> None | str:
prompt = refine_characters(prompt)
diff --git a/opentrons-ai-server/api/domain/prompts.py b/opentrons-ai-server/api/domain/prompts.py
index 582bd1565ea..8d335b65227 100644
--- a/opentrons-ai-server/api/domain/prompts.py
+++ b/opentrons-ai-server/api/domain/prompts.py
@@ -1,15 +1,16 @@
import json
-import logging
import uuid
from typing import Any, Dict, Iterable
import requests
+import structlog
+from ddtrace import tracer
from openai.types.chat import ChatCompletionToolParam
from api.settings import Settings
settings: Settings = Settings()
-logger = logging.getLogger(__name__)
+logger = structlog.stdlib.get_logger(settings.logger_name)
def generate_unique_name() -> str:
@@ -17,13 +18,14 @@ def generate_unique_name() -> str:
return unique_name
+@tracer.wrap()
def send_post_request(payload: str) -> str:
url = "https://Opentrons-simulator.hf.space/protocol"
protocol_name: str = generate_unique_name()
data = {"name": protocol_name, "content": payload}
hf_token: str = settings.huggingface_api_key.get_secret_value()
headers = {"Content-Type": "application/json", "Authorization": "Bearer {}".format(hf_token)}
-
+ logger.info("Sending POST request to the simulate API", extra={"url": url, "protocolName": data["name"]})
response = requests.post(url, json=data, headers=headers)
if response.status_code != 200:
diff --git a/opentrons-ai-server/api/handler/custom_logging.py b/opentrons-ai-server/api/handler/custom_logging.py
new file mode 100644
index 00000000000..a062528f803
--- /dev/null
+++ b/opentrons-ai-server/api/handler/custom_logging.py
@@ -0,0 +1,133 @@
+# Taken directly from https://gist.github.com/nymous/f138c7f06062b7c43c060bf03759c29e
+import logging
+import sys
+
+import ddtrace
+import structlog
+from ddtrace import tracer
+from structlog.types import EventDict, Processor
+
+
+# https://github.com/hynek/structlog/issues/35#issuecomment-591321744
+def rename_event_key(_, __, event_dict: EventDict) -> EventDict: # type: ignore[no-untyped-def]
+ """
+ Log entries keep the text message in the `event` field, but Datadog
+ uses the `message` field. This processor moves the value from one field to
+ the other.
+ See https://github.com/hynek/structlog/issues/35#issuecomment-591321744
+ """
+ event_dict["message"] = event_dict.pop("event")
+ return event_dict
+
+
+def drop_color_message_key(_, __, event_dict: EventDict) -> EventDict: # type: ignore[no-untyped-def]
+ """
+ Uvicorn logs the message a second time in the extra `color_message`, but we don't
+ need it. This processor drops the key from the event dict if it exists.
+ """
+ event_dict.pop("color_message", None)
+ return event_dict
+
+
+def tracer_injection(_, __, event_dict: EventDict) -> EventDict: # type: ignore[no-untyped-def]
+ # get correlation ids from current tracer context
+ span = tracer.current_span()
+ trace_id, span_id = (str((1 << 64) - 1 & span.trace_id), span.span_id) if span else (None, None)
+
+ # add ids to structlog event dictionary
+ event_dict["dd.trace_id"] = str(trace_id or 0)
+ event_dict["dd.span_id"] = str(span_id or 0)
+
+ # add the env, service, and version configured for the tracer
+ event_dict["dd.env"] = ddtrace.config.env or ""
+ event_dict["dd.service"] = ddtrace.config.service or ""
+ event_dict["dd.version"] = ddtrace.config.version or ""
+
+ return event_dict
+
+
+def setup_logging(json_logs: bool = False, log_level: str = "INFO") -> None:
+ timestamper = structlog.processors.TimeStamper(fmt="iso")
+
+ shared_processors: list[Processor] = [
+ structlog.contextvars.merge_contextvars,
+ structlog.stdlib.add_logger_name,
+ structlog.stdlib.add_log_level,
+ structlog.stdlib.PositionalArgumentsFormatter(),
+ structlog.stdlib.ExtraAdder(),
+ drop_color_message_key,
+ tracer_injection,
+ timestamper,
+ structlog.processors.StackInfoRenderer(),
+ ]
+
+ if json_logs:
+ # We rename the `event` key to `message` only in JSON logs, as Datadog looks for the
+ # `message` key but the pretty ConsoleRenderer looks for `event`
+ shared_processors.append(rename_event_key)
+ # Format the exception only for JSON logs, as we want to pretty-print them when
+ # using the ConsoleRenderer
+ shared_processors.append(structlog.processors.format_exc_info)
+
+ structlog.configure(
+ processors=shared_processors
+ + [
+ # Prepare event dict for `ProcessorFormatter`.
+ structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
+ ],
+ logger_factory=structlog.stdlib.LoggerFactory(),
+ cache_logger_on_first_use=True,
+ )
+
+ log_renderer: structlog.types.Processor
+ if json_logs:
+ log_renderer = structlog.processors.JSONRenderer()
+ else:
+ log_renderer = structlog.dev.ConsoleRenderer()
+
+ formatter = structlog.stdlib.ProcessorFormatter(
+ # These run ONLY on `logging` entries that do NOT originate within
+ # structlog.
+ foreign_pre_chain=shared_processors,
+ # These run on ALL entries after the pre_chain is done.
+ processors=[
+ # Remove _record & _from_structlog.
+ structlog.stdlib.ProcessorFormatter.remove_processors_meta,
+ log_renderer,
+ ],
+ )
+
+ handler = logging.StreamHandler()
+ # Use OUR `ProcessorFormatter` to format all `logging` entries.
+ handler.setFormatter(formatter)
+ root_logger = logging.getLogger()
+ root_logger.addHandler(handler)
+ root_logger.setLevel(log_level.upper())
+
+ for _log in ["uvicorn", "uvicorn.error"]:
+ # Clear the log handlers for uvicorn loggers, and enable propagation
+ # so the messages are caught by our root logger and formatted correctly
+ # by structlog
+ logging.getLogger(_log).handlers.clear()
+ logging.getLogger(_log).propagate = True
+
+ # Since we re-create the access logs ourselves, to add all information
+ # in the structured log (see the `logging_middleware` in main.py), we clear
+ # the handlers and prevent the logs to propagate to a logger higher up in the
+ # hierarchy (effectively rendering them silent).
+ logging.getLogger("uvicorn.access").handlers.clear()
+ logging.getLogger("uvicorn.access").propagate = False
+
+ def handle_exception(exc_type, exc_value, exc_traceback): # type: ignore[no-untyped-def]
+ """
+ Log any uncaught exception instead of letting it be printed by Python
+ (but leave KeyboardInterrupt untouched to allow users to Ctrl+C to stop)
+ See https://stackoverflow.com/a/16993115/3641865
+ """
+ if issubclass(exc_type, KeyboardInterrupt):
+ sys.__excepthook__(exc_type, exc_value, exc_traceback)
+ return
+
+ root_logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
+
+ sys.excepthook = handle_exception
diff --git a/opentrons-ai-server/api/handler/fast.py b/opentrons-ai-server/api/handler/fast.py
index 3c88e08a1a2..8e5572d0c15 100644
--- a/opentrons-ai-server/api/handler/fast.py
+++ b/opentrons-ai-server/api/handler/fast.py
@@ -1,9 +1,13 @@
import asyncio
import os
+import time
from typing import Any, Awaitable, Callable, List, Literal, Union
-import ddtrace
+import structlog
+from asgi_correlation_id import CorrelationIdMiddleware
+from asgi_correlation_id.context import correlation_id
from ddtrace import tracer
+from ddtrace.contrib.asgi.middleware import TraceMiddleware
from fastapi import FastAPI, HTTPException, Query, Request, Response, Security, status
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
@@ -11,9 +15,10 @@
from fastapi.responses import HTMLResponse, JSONResponse
from pydantic import BaseModel, Field, conint
from starlette.middleware.base import BaseHTTPMiddleware
+from uvicorn.protocols.utils import get_path_with_query_string
from api.domain.openai_predict import OpenAIPredict
-from api.handler.logging_config import get_logger, setup_logging
+from api.handler.custom_logging import setup_logging
from api.integration.auth import VerifyToken
from api.models.chat_request import ChatRequest
from api.models.chat_response import ChatResponse
@@ -21,10 +26,12 @@
from api.models.internal_server_error import InternalServerError
from api.settings import Settings
-setup_logging()
-logger = get_logger(__name__)
-ddtrace.patch(logging=True)
settings: Settings = Settings()
+setup_logging(json_logs=settings.json_logging, log_level=settings.log_level.upper())
+
+access_logger = structlog.stdlib.get_logger("api.access")
+logger = structlog.stdlib.get_logger(settings.logger_name)
+
auth: VerifyToken = VerifyToken()
openai: OpenAIPredict = OpenAIPredict(settings)
@@ -75,6 +82,61 @@ async def dispatch(self, request: Request, call_next: Any) -> JSONResponse | Any
app.add_middleware(TimeoutMiddleware, timeout_s=178)
+@app.middleware("http")
+async def logging_middleware(request: Request, call_next) -> Response: # type: ignore[no-untyped-def]
+ structlog.contextvars.clear_contextvars()
+ # These context vars will be added to all log entries emitted during the request
+ request_id = correlation_id.get()
+ structlog.contextvars.bind_contextvars(request_id=request_id)
+
+ start_time = time.perf_counter_ns()
+ # If the call_next raises an error, we still want to return our own 500 response,
+ # so we can add headers to it (process time, request ID...)
+ response = Response(status_code=500)
+ try:
+ response = await call_next(request)
+ except Exception:
+ structlog.stdlib.get_logger("api.error").exception("Uncaught exception")
+ raise
+ finally:
+ process_time = time.perf_counter_ns() - start_time
+ status_code = response.status_code
+ url = get_path_with_query_string(request.scope) # type: ignore[arg-type]
+ client_host = request.client.host if request.client and request.client.host else "unknown"
+ client_port = request.client.port if request.client and request.client.port else "unknown"
+ http_method = request.method if request.method else "unknown"
+ http_version = request.scope["http_version"]
+ # Recreate the Uvicorn access log format, but add all parameters as structured information
+ access_logger.info(
+ f"""{client_host}:{client_port} - "{http_method} {url} HTTP/{http_version}" {status_code}""",
+ http={
+ "url": str(request.url),
+ "status_code": status_code,
+ "method": http_method,
+ "request_id": request_id,
+ "version": http_version,
+ },
+ network={"client": {"ip": client_host, "port": client_port}},
+ duration=process_time,
+ )
+ response.headers["X-Process-Time"] = str(process_time / 10**9)
+ return response
+
+
+# This middleware must be placed after the logging, to populate the context with the request ID
+# NOTE: Why last??
+# Answer: middlewares are applied in the reverse order of when they are added (you can verify this
+# by debugging `app.middleware_stack` and recursively drilling down the `app` property).
+app.add_middleware(CorrelationIdMiddleware)
+
+tracing_middleware = next((m for m in app.user_middleware if m.cls == TraceMiddleware), None)
+if tracing_middleware is not None:
+ app.user_middleware = [m for m in app.user_middleware if m.cls != TraceMiddleware]
+ structlog.stdlib.get_logger("api.datadog_patch").info("Patching Datadog tracing middleware to be the outermost middleware...")
+ app.user_middleware.insert(0, tracing_middleware)
+ app.middleware_stack = app.build_middleware_stack()
+
+
# Models
class Status(BaseModel):
status: Literal["ok", "error"]
@@ -134,7 +196,7 @@ async def create_chat_completion(
return ChatResponse(reply=response, fake=body.fake)
except Exception as e:
- logger.exception(e)
+ logger.exception("Error processing chat completion")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=InternalServerError(exception_object=e).model_dump()
) from e
@@ -143,7 +205,7 @@ async def create_chat_completion(
@app.get(
"/health",
response_model=Status,
- summary="LB Health Check",
+ summary="Load Balancer Health Check",
description="Check the health and version of the API.",
include_in_schema=False,
)
@@ -154,10 +216,14 @@ async def get_health(request: Request) -> Status:
- **returns**: A Status containing the version of the API.
"""
- logger.debug(f"{request.method} {request.url.path}")
+ if request.url.path == "/health":
+ pass # This is a health check from the load balancer
+ else:
+ logger.info(f"{request.method} {request.url.path}", extra={"requestMethod": request.method, "requestPath": request.url.path})
return Status(status="ok", version=settings.dd_version)
+@tracer.wrap()
@app.get("/api/timeout", response_model=TimeoutResponse)
async def timeout_endpoint(request: Request, seconds: conint(ge=1, le=300) = Query(..., description="Number of seconds to wait")): # type: ignore # noqa: B008
"""
diff --git a/opentrons-ai-server/api/handler/local_run.py b/opentrons-ai-server/api/handler/local_run.py
index 0b82fae7e41..e9bbcc6f151 100644
--- a/opentrons-ai-server/api/handler/local_run.py
+++ b/opentrons-ai-server/api/handler/local_run.py
@@ -1,9 +1,12 @@
# run.py
import uvicorn
-from api.handler.logging_config import setup_logging
-
-setup_logging()
-
if __name__ == "__main__":
- uvicorn.run("api.handler.fast:app", host="localhost", port=8000, timeout_keep_alive=190, reload=True)
+ uvicorn.run(
+ "api.handler.fast:app",
+ host="localhost",
+ port=8000,
+ timeout_keep_alive=190,
+ reload=True,
+ log_config=None,
+ )
diff --git a/opentrons-ai-server/api/handler/logging_config.py b/opentrons-ai-server/api/handler/logging_config.py
deleted file mode 100644
index fc576b6ad80..00000000000
--- a/opentrons-ai-server/api/handler/logging_config.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# logging_config.py
-import logging
-
-from pythonjsonlogger import jsonlogger
-
-from api.settings import Settings
-
-FORMAT = (
- "%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] "
- "[dd.service=%(dd.service)s dd.env=%(dd.env)s dd.version=%(dd.version)s dd.trace_id=%(dd.trace_id)s dd.span_id=%(dd.span_id)s] "
- "- %(message)s"
-)
-
-LOCAL_FORMAT = "%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] - %(message)s"
-
-
-def setup_logging() -> None:
- settings = Settings()
- log_handler = logging.StreamHandler()
-
- if settings.environment == "local":
- formatter = logging.Formatter(LOCAL_FORMAT)
- else:
- formatter = jsonlogger.JsonFormatter(FORMAT) # type: ignore
-
- log_handler.setFormatter(formatter)
-
- logging.basicConfig(
- level=settings.log_level.upper(),
- handlers=[log_handler],
- )
-
-
-# Call this function to initialize logging
-setup_logging()
-
-
-def get_logger(name: str) -> logging.Logger:
- return logging.getLogger(name)
diff --git a/opentrons-ai-server/api/integration/auth.py b/opentrons-ai-server/api/integration/auth.py
index c3cf4b8d163..12e8b2a4a9e 100644
--- a/opentrons-ai-server/api/integration/auth.py
+++ b/opentrons-ai-server/api/integration/auth.py
@@ -1,13 +1,14 @@
-import logging
from typing import Any, Optional
import jwt
+import structlog
from fastapi import HTTPException, Security, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer, SecurityScopes
from api.settings import Settings
-logger = logging.getLogger(__name__)
+settings: Settings = Settings()
+logger = structlog.stdlib.get_logger(settings.logger_name)
class UnauthenticatedException(HTTPException):
@@ -35,10 +36,10 @@ async def verify(
try:
signing_key = self.jwks_client.get_signing_key_from_jwt(credentials.credentials).key
except jwt.PyJWKClientError as error:
- logger.error(error, extra={"credentials": credentials})
+ logger.error("Client Error", extra={"credentials": credentials}, exc_info=True)
raise UnauthenticatedException() from error
except jwt.exceptions.DecodeError as error:
- logger.error(error, extra={"credentials": credentials})
+ logger.error("Decode Error", extra={"credentials": credentials}, exc_info=True)
raise UnauthenticatedException() from error
try:
@@ -51,10 +52,10 @@ async def verify(
)
logger.info("Decoded token", extra={"token": payload})
return payload
- except jwt.ExpiredSignatureError as error:
- logger.error(error, extra={"credentials": credentials})
+ except jwt.ExpiredSignatureError:
+ logger.error("Expired Signature", extra={"credentials": credentials}, exc_info=True)
# Handle token expiration, e.g., refresh token, re-authenticate, etc.
- except jwt.PyJWTError as error:
- logger.error(error, extra={"credentials": credentials})
+ except jwt.PyJWTError:
+ logger.error("General JWT Error", extra={"credentials": credentials}, exc_info=True)
# Handle other JWT errors
raise UnauthenticatedException()
diff --git a/opentrons-ai-server/api/settings.py b/opentrons-ai-server/api/settings.py
index c29f44aface..c59a25c33de 100644
--- a/opentrons-ai-server/api/settings.py
+++ b/opentrons-ai-server/api/settings.py
@@ -1,3 +1,4 @@
+import os
from pathlib import Path
from pydantic import SecretStr
@@ -6,6 +7,10 @@
ENV_PATH: Path = Path(Path(__file__).parent.parent, ".env")
+def is_running_in_docker() -> bool:
+ return os.path.exists("/.dockerenv")
+
+
class Settings(BaseSettings):
"""
If the env_file file exists: It will read the configurations from the env_file file (local execution)
@@ -26,7 +31,7 @@ class Settings(BaseSettings):
auth0_algorithms: str = "RS256"
dd_version: str = "hardcoded_default_from_settings"
allowed_origins: str = "*"
- dd_logs_injection: str = "true"
+ dd_trace_enabled: str = "false"
cpu: str = "1028"
memory: str = "2048"
@@ -35,6 +40,16 @@ class Settings(BaseSettings):
openai_api_key: SecretStr = SecretStr("default_openai_api_key")
huggingface_api_key: SecretStr = SecretStr("default_huggingface_api_key")
+ @property
+ def json_logging(self) -> bool:
+ if self.environment == "local" and not is_running_in_docker():
+ return False
+ return True
+
+ @property
+ def logger_name(self) -> str:
+ return "app.logger"
+
def get_settings_from_json(json_str: str) -> Settings:
"""
diff --git a/opentrons-ai-server/api/uvicorn_disable_logging.json b/opentrons-ai-server/api/uvicorn_disable_logging.json
new file mode 100644
index 00000000000..e2f06cb503f
--- /dev/null
+++ b/opentrons-ai-server/api/uvicorn_disable_logging.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "disable_existing_loggers": false,
+ "formatters": {
+ "default": {
+ "()": "uvicorn.logging.DefaultFormatter",
+ "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+ },
+ "access": {
+ "()": "uvicorn.logging.AccessFormatter",
+ "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+ }
+ },
+ "handlers": {
+ "default": {
+ "formatter": "default",
+ "class": "logging.NullHandler"
+ },
+ "access": {
+ "formatter": "access",
+ "class": "logging.NullHandler"
+ }
+ },
+ "loggers": {
+ "uvicorn.error": {
+ "level": "INFO",
+ "handlers": ["default"],
+ "propagate": false
+ },
+ "uvicorn.access": {
+ "level": "INFO",
+ "handlers": ["access"],
+ "propagate": false
+ }
+ }
+}
diff --git a/opentrons-ai-server/tests/helpers/huggingface_client.py b/opentrons-ai-server/tests/helpers/huggingface_client.py
index 7b66fd61674..a55792d2fb7 100644
--- a/opentrons-ai-server/tests/helpers/huggingface_client.py
+++ b/opentrons-ai-server/tests/helpers/huggingface_client.py
@@ -39,6 +39,7 @@ def get_auth_headers(self, token_override: str | None = None) -> dict[str, str]:
return {"Authorization": f"Bearer {self.settings.HF_API_KEY}"}
def post_simulate_protocol(self, protocol: Protocol) -> Response:
+ console.print(self.auth_headers)
return self.httpx.post("https://opentrons-simulator.hf.space/protocol", headers=self.standard_headers, json=protocol.model_dump())
diff --git a/protocol-designer/src/assets/localization/en/form.json b/protocol-designer/src/assets/localization/en/form.json
index aaa000a1668..65267079c30 100644
--- a/protocol-designer/src/assets/localization/en/form.json
+++ b/protocol-designer/src/assets/localization/en/form.json
@@ -97,7 +97,7 @@
"pickUp": "pick up tip"
},
"magnetAction": {
- "label": "Magnet action",
+ "label": "Magnet state",
"options": {
"disengage": "Disengage",
"engage": "Engage"
diff --git a/protocol-designer/src/components/StepEditForm/forms/__tests__/MagnetForm.test.tsx b/protocol-designer/src/components/StepEditForm/forms/__tests__/MagnetForm.test.tsx
index 198f2c68bd2..d844ef132a7 100644
--- a/protocol-designer/src/components/StepEditForm/forms/__tests__/MagnetForm.test.tsx
+++ b/protocol-designer/src/components/StepEditForm/forms/__tests__/MagnetForm.test.tsx
@@ -78,7 +78,7 @@ describe('MagnetForm', () => {
screen.getByText('magnet')
screen.getByText('module')
screen.getByText('mock name')
- screen.getByText('Magnet action')
+ screen.getByText('Magnet state')
const engage = screen.getByText('Engage')
screen.getByText('Disengage')
fireEvent.click(engage)
diff --git a/protocol-designer/src/molecules/ToggleExpandStepFormField/index.tsx b/protocol-designer/src/molecules/ToggleExpandStepFormField/index.tsx
index 29977196a3d..ed57de37f3b 100644
--- a/protocol-designer/src/molecules/ToggleExpandStepFormField/index.tsx
+++ b/protocol-designer/src/molecules/ToggleExpandStepFormField/index.tsx
@@ -23,6 +23,7 @@ interface ToggleExpandStepFormFieldProps extends FieldProps {
toggleUpdateValue: (value: unknown) => void
toggleValue: unknown
caption?: string
+ islabel?: boolean
}
export function ToggleExpandStepFormField(
props: ToggleExpandStepFormFieldProps
@@ -37,6 +38,7 @@ export function ToggleExpandStepFormField(
toggleUpdateValue,
toggleValue,
caption,
+ islabel,
...restProps
} = props
@@ -58,13 +60,24 @@ export function ToggleExpandStepFormField(
>
{title}
- {
- onToggleUpdateValue()
- }}
- label={isSelected ? onLabel : offLabel}
- toggledOn={isSelected}
- />
+
+ {islabel ? (
+
+ {isSelected ? onLabel : offLabel}
+
+ ) : null}
+
+ {
+ onToggleUpdateValue()
+ }}
+ label={isSelected ? onLabel : offLabel}
+ toggledOn={isSelected}
+ />
+
{isSelected ? (
diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MagnetTools/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MagnetTools/index.tsx
index 7f7afd9702a..e32bbd860fb 100644
--- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MagnetTools/index.tsx
+++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MagnetTools/index.tsx
@@ -3,13 +3,17 @@ import { useTranslation } from 'react-i18next'
import {
COLORS,
DIRECTION_COLUMN,
+ DeckInfoLabel,
Divider,
Flex,
ListItem,
SPACING,
StyledText,
} from '@opentrons/components'
-import { MAGNETIC_MODULE_V1 } from '@opentrons/shared-data'
+import {
+ MAGNETIC_MODULE_TYPE,
+ MAGNETIC_MODULE_V1,
+} from '@opentrons/shared-data'
import {
MAX_ENGAGE_HEIGHT_V1,
MAX_ENGAGE_HEIGHT_V2,
@@ -21,7 +25,11 @@ import {
getMagneticLabwareOptions,
} from '../../../../../../ui/modules/selectors'
import { ToggleExpandStepFormField } from '../../../../../../molecules'
-import { getModuleEntities } from '../../../../../../step-forms/selectors'
+import {
+ getInitialDeckSetup,
+ getModuleEntities,
+} from '../../../../../../step-forms/selectors'
+import { getModulesOnDeckByType } from '../../../../../../ui/modules/utils'
import type { StepFormProps } from '../../types'
@@ -31,8 +39,16 @@ export function MagnetTools(props: StepFormProps): JSX.Element {
const moduleLabwareOptions = useSelector(getMagneticLabwareOptions)
const moduleEntities = useSelector(getModuleEntities)
const defaultEngageHeight = useSelector(getMagnetLabwareEngageHeight)
+ const deckSetup = useSelector(getInitialDeckSetup)
+ const modulesOnDeck = getModulesOnDeckByType(deckSetup, MAGNETIC_MODULE_TYPE)
+
+ console.log(modulesOnDeck)
+
const moduleModel = moduleEntities[formData.moduleId].model
+ const slotInfo = moduleLabwareOptions[0].name.split('in')
+ const slotLocation = modulesOnDeck != null ? modulesOnDeck[0].slot : ''
+
const mmUnits = t('units.millimeter')
const isGen1 = moduleModel === MAGNETIC_MODULE_V1
const engageHeightMinMax = isGen1
@@ -53,7 +69,7 @@ export function MagnetTools(props: StepFormProps): JSX.Element {
})
: ''
const engageHeightCaption = `${engageHeightMinMax} ${engageHeightDefault}`
-
+ // TODO (10-9-2024): Replace ListItem with ListItemDescriptor
return (
-
-
- {moduleLabwareOptions[0].name}
-
+
+
+
+
+
+
+ {slotInfo[0]}
+
+
+ {slotInfo[1]}
+
+
@@ -88,6 +115,7 @@ export function MagnetTools(props: StepFormProps): JSX.Element {
'form:step_edit_form.field.magnetAction.options.disengage'
)}
caption={engageHeightCaption}
+ islabel={true}
/>
diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/__tests__/MagnetTools.test.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/__tests__/MagnetTools.test.tsx
index 968c523977e..5a901290c37 100644
--- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/__tests__/MagnetTools.test.tsx
+++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/__tests__/MagnetTools.test.tsx
@@ -6,7 +6,10 @@ import {
getMagneticLabwareOptions,
getMagnetLabwareEngageHeight,
} from '../../../../../../ui/modules/selectors'
-import { getModuleEntities } from '../../../../../../step-forms/selectors'
+import {
+ getInitialDeckSetup,
+ getModuleEntities,
+} from '../../../../../../step-forms/selectors'
import { MagnetTools } from '../MagnetTools'
import type { ComponentProps } from 'react'
import type * as ModulesSelectors from '../../../../../../ui/modules/selectors'
@@ -67,7 +70,7 @@ describe('MagnetTools', () => {
},
}
vi.mocked(getMagneticLabwareOptions).mockReturnValue([
- { name: 'mock name', value: 'mockValue' },
+ { name: 'mock labware in mock module in slot abc', value: 'mockValue' },
])
vi.mocked(getModuleEntities).mockReturnValue({
magnetId: {
@@ -77,13 +80,29 @@ describe('MagnetTools', () => {
},
})
vi.mocked(getMagnetLabwareEngageHeight).mockReturnValue(null)
+ vi.mocked(getInitialDeckSetup).mockReturnValue({
+ labware: {},
+ modules: {
+ module: {
+ id: 'mockId',
+ slot: '10',
+ type: 'magneticModuleType',
+ moduleState: { engaged: false, type: 'magneticModuleType' },
+ model: 'magneticModuleV1',
+ },
+ },
+ additionalEquipmentOnDeck: {},
+ pipettes: {},
+ })
})
it('renders the text and a switch button for v2', () => {
render(props)
screen.getByText('Module')
- screen.getByText('mock name')
- screen.getByText('Magnet action')
+ screen.getByText('10')
+ screen.getByText('mock labware')
+ screen.getByText('mock module')
+ screen.getByText('Magnet state')
screen.getByLabelText('Engage')
const toggleButton = screen.getByRole('switch')
screen.getByText('Engage height')
diff --git a/robot-server/robot_server/runs/router/labware_router.py b/robot-server/robot_server/runs/router/labware_router.py
index 7eba96afa0e..16924fd4ae8 100644
--- a/robot-server/robot_server/runs/router/labware_router.py
+++ b/robot-server/robot_server/runs/router/labware_router.py
@@ -16,7 +16,6 @@
RequestModel,
SimpleBody,
PydanticResponse,
- ResponseList,
)
from ..run_models import Run, LabwareDefinitionSummary
@@ -86,7 +85,7 @@ async def add_labware_offset(
),
status_code=status.HTTP_201_CREATED,
responses={
- status.HTTP_201_CREATED: {"model": SimpleBody[Run]},
+ status.HTTP_201_CREATED: {"model": SimpleBody[LabwareDefinitionSummary]},
status.HTTP_404_NOT_FOUND: {"model": ErrorBody[RunNotFound]},
status.HTTP_409_CONFLICT: {"model": ErrorBody[Union[RunStopped, RunNotIdle]]},
},
@@ -134,14 +133,14 @@ async def add_labware_definition(
" Repeated definitions will be deduplicated."
),
responses={
- status.HTTP_200_OK: {"model": SimpleBody[Run]},
+ status.HTTP_200_OK: {"model": SimpleBody[list[SD_LabwareDefinition]]},
status.HTTP_409_CONFLICT: {"model": ErrorBody[RunStopped]},
},
)
async def get_run_loaded_labware_definitions(
runId: str,
run_data_manager: Annotated[RunDataManager, Depends(get_run_data_manager)],
-) -> PydanticResponse[SimpleBody[ResponseList[SD_LabwareDefinition]]]:
+) -> PydanticResponse[SimpleBody[list[SD_LabwareDefinition]]]:
"""Get a run's loaded labware definition by the run ID.
Args:
@@ -155,8 +154,7 @@ async def get_run_loaded_labware_definitions(
except RunNotCurrentError as e:
raise RunStopped(detail=str(e)).as_error(status.HTTP_409_CONFLICT) from e
- labware_definitions_result = ResponseList.construct(__root__=labware_definitions)
return await PydanticResponse.create(
- content=SimpleBody.construct(data=labware_definitions_result),
+ content=SimpleBody.construct(data=labware_definitions),
status_code=status.HTTP_200_OK,
)
diff --git a/robot-server/robot_server/service/json_api/__init__.py b/robot-server/robot_server/service/json_api/__init__.py
index 2680c99049f..78a9deeaa4d 100644
--- a/robot-server/robot_server/service/json_api/__init__.py
+++ b/robot-server/robot_server/service/json_api/__init__.py
@@ -14,7 +14,6 @@
DeprecatedResponseDataModel,
ResourceModel,
PydanticResponse,
- ResponseList,
NotifyRefetchBody,
NotifyUnsubscribeBody,
)
@@ -44,7 +43,6 @@
"DeprecatedResponseDataModel",
"DeprecatedResponseModel",
"DeprecatedMultiResponseModel",
- "ResponseList",
# notify models
"NotifyRefetchBody",
"NotifyUnsubscribeBody",
diff --git a/robot-server/robot_server/service/json_api/response.py b/robot-server/robot_server/service/json_api/response.py
index e1e422f255c..8764d8edd53 100644
--- a/robot-server/robot_server/service/json_api/response.py
+++ b/robot-server/robot_server/service/json_api/response.py
@@ -278,12 +278,6 @@ class DeprecatedMultiResponseModel(
)
-class ResponseList(BaseModel, Generic[ResponseDataT]):
- """A response that returns a list resource."""
-
- __root__: List[ResponseDataT]
-
-
class NotifyRefetchBody(BaseResponseBody):
"""A notification response that returns a flag for refetching via HTTP."""
diff --git a/robot-server/tests/runs/router/test_labware_router.py b/robot-server/tests/runs/router/test_labware_router.py
index 9a38ce6cd0f..1e3b929446d 100644
--- a/robot-server/tests/runs/router/test_labware_router.py
+++ b/robot-server/tests/runs/router/test_labware_router.py
@@ -169,7 +169,7 @@ async def test_get_run_labware_definition(
runId="run-id", run_data_manager=mock_run_data_manager
)
- assert result.content.data.__root__ == [
+ assert result.content.data == [
SD_LabwareDefinition.construct(namespace="test_1"), # type: ignore[call-arg]
SD_LabwareDefinition.construct(namespace="test_2"), # type: ignore[call-arg]
]
diff --git a/shared-data/js/__tests__/labwareDefSchemaV3.test.ts b/shared-data/js/__tests__/labwareDefSchemaV3.test.ts
index 8416e8b60c5..14d0c4bf968 100644
--- a/shared-data/js/__tests__/labwareDefSchemaV3.test.ts
+++ b/shared-data/js/__tests__/labwareDefSchemaV3.test.ts
@@ -33,14 +33,10 @@ const checkGeometryDefinitions = (
expect(wellGeometryId in labwareDef.innerLabwareGeometry).toBe(true)
const wellDepth = labwareDef.wells[wellName].depth
- const wellShape = labwareDef.wells[wellName].shape
const topFrustumHeight =
- labwareDef.innerLabwareGeometry[wellGeometryId].frusta[0].topHeight
- const topFrustumShape =
- labwareDef.innerLabwareGeometry[wellGeometryId].frusta[0].shape
+ labwareDef.innerLabwareGeometry[wellGeometryId].sections[0].topHeight
expect(wellDepth).toEqual(topFrustumHeight)
- expect(wellShape).toEqual(topFrustumShape)
}
})
}
diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts
index dbd8c7f59c7..0ffb3f7a649 100644
--- a/shared-data/js/types.ts
+++ b/shared-data/js/types.ts
@@ -162,25 +162,57 @@ export type LabwareWell = LabwareWellProperties & {
export interface SphericalSegment {
shape: 'spherical'
radiusOfCurvature: number
- depth: number
+ topHeight: number
+ bottomHeight: number
}
-export interface CircularBoundedSection {
- shape: 'circular'
- diameter: number
+export interface ConicalFrustum {
+ shape: 'conical'
+ bottomDiameter: number
+ topDiameter: number
topHeight: number
+ bottomHeight: number
}
-export interface RectangularBoundedSection {
- shape: 'rectangular'
- xDimension: number
- yDimension: number
+export interface CuboidalFrustum {
+ shape: 'cuboidal'
+ bottomXDimension: number
+ bottomYDimension: number
+ topXDimension: number
+ topYDimension: number
topHeight: number
+ bottomHeight: number
}
+export interface SquaredConeSegment {
+ shape: 'squaredcone'
+ bottomCrossSection: string
+ circleDiameter: number
+ rectangleXDimension: number
+ rectangleYDimension: number
+ topHeight: number
+ bottomHeight: number
+}
+
+export interface RoundedCuboidSegment {
+ shape: 'roundedcuboid'
+ bottomCrossSection: string
+ circleDiameter: number
+ rectangleXDimension: number
+ rectangleYDimension: number
+ topHeight: number
+ bottomHeight: number
+}
+
+export type WellSegment =
+ | CuboidalFrustum
+ | ConicalFrustum
+ | SquaredConeSegment
+ | SphericalSegment
+ | RoundedCuboidSegment
+
export interface InnerWellGeometry {
- frusta: CircularBoundedSection[] | RectangularBoundedSection[]
- bottomShape?: SphericalSegment | null
+ sections: WellSegment[]
}
// TODO(mc, 2019-03-21): exact object is tough to use with the initial value in
diff --git a/shared-data/labware/fixtures/3/fixture_2_plate.json b/shared-data/labware/fixtures/3/fixture_2_plate.json
index a2e1bb5a3ea..19ea2f82ffc 100644
--- a/shared-data/labware/fixtures/3/fixture_2_plate.json
+++ b/shared-data/labware/fixtures/3/fixture_2_plate.json
@@ -62,39 +62,34 @@
},
"innerLabwareGeometry": {
"daiwudhadfhiew": {
- "frusta": [
+ "sections": [
{
- "shape": "rectangular",
- "xDimension": 127.76,
- "yDimension": 85.8,
- "topHeight": 42.16
- },
- {
- "shape": "rectangular",
- "xDimension": 70.0,
- "yDimension": 50.0,
- "topHeight": 20.0
+ "shape": "cuboidal",
+ "topXDimension": 127.76,
+ "topYDimension": 85.8,
+ "bottomXDimension": 70.0,
+ "bottomYDimension": 50.0,
+ "topHeight": 42.16,
+ "bottomHeight": 20.0
}
]
},
"iuweofiuwhfn": {
- "frusta": [
+ "sections": [
{
- "shape": "circular",
- "diameter": 35.0,
- "topHeight": 42.16
+ "shape": "conical",
+ "bottomDiameter": 35.0,
+ "topDiameter": 35.0,
+ "topHeight": 42.16,
+ "bottomHeight": 10.0
},
{
- "shape": "circular",
- "diameter": 35.0,
- "topHeight": 20.0
+ "shape": "spherical",
+ "radiusOfCurvature": 20.0,
+ "topHeight": 10.0,
+ "bottomHeight": 0.0
}
- ],
- "bottomShape": {
- "shape": "spherical",
- "radiusOfCurvature": 20.0,
- "depth": 6.0
- }
+ ]
}
}
}
diff --git a/shared-data/labware/fixtures/3/fixture_corning_24_plate.json b/shared-data/labware/fixtures/3/fixture_corning_24_plate.json
index d53a6f017ca..679f8916377 100644
--- a/shared-data/labware/fixtures/3/fixture_corning_24_plate.json
+++ b/shared-data/labware/fixtures/3/fixture_corning_24_plate.json
@@ -323,16 +323,13 @@
},
"innerLabwareGeometry": {
"venirhgerug": {
- "frusta": [
+ "sections": [
{
- "shape": "circular",
- "diameter": 16.26,
- "topHeight": 17.4
- },
- {
- "shape": "circular",
- "diameter": 16.26,
- "topHeight": 0.0
+ "shape": "conical",
+ "bottomDiameter": 16.26,
+ "topDiameter": 16.26,
+ "topHeight": 17.4,
+ "bottomHeight": 0.0
}
]
}
diff --git a/shared-data/labware/schemas/3.json b/shared-data/labware/schemas/3.json
index e03b1c8f064..ecd285c554a 100644
--- a/shared-data/labware/schemas/3.json
+++ b/shared-data/labware/schemas/3.json
@@ -67,8 +67,9 @@
},
"SphericalSegment": {
"type": "object",
+ "description": "A partial sphere shaped section at the bottom of the well.",
"additionalProperties": false,
- "required": ["shape", "radiusOfCurvature", "depth"],
+ "required": ["shape", "radiusOfCurvature", "topHeight", "bottomHeight"],
"properties": {
"shape": {
"type": "string",
@@ -77,70 +78,182 @@
"radiusOfCurvature": {
"type": "number"
},
- "depth": {
+ "topHeight": {
+ "type": "number"
+ },
+ "bottomHeight": {
"type": "number"
}
}
},
- "CircularBoundedSection": {
+ "ConicalFrustum": {
"type": "object",
- "required": ["shape", "diameter", "topHeight"],
+ "description": "A cone or conical segment, bounded by two circles on the top and bottom.",
+ "required": [
+ "shape",
+ "bottomDiameter",
+ "topDiameter",
+ "topHeight",
+ "bottomHeight"
+ ],
"properties": {
"shape": {
"type": "string",
- "enum": ["circular"]
+ "enum": ["conical"]
},
- "diameter": {
+ "bottomDiameter": {
+ "type": "number"
+ },
+ "topDiameter": {
"type": "number"
},
"topHeight": {
- "type": "number",
- "description": "The height at the top of a bounded subsection of a well, relative to the bottom"
+ "type": "number"
+ },
+ "bottomHeight": {
+ "type": "number"
}
}
},
- "RectangularBoundedSection": {
+ "CuboidalFrustum": {
"type": "object",
- "required": ["shape", "xDimension", "yDimension", "topHeight"],
+ "description": "A cuboidal shape bounded by two rectangles on the top and bottom",
+ "required": [
+ "shape",
+ "bottomXDimension",
+ "bottomYDimension",
+ "topXDimension",
+ "topYDimension",
+ "topHeight",
+ "bottomHeight"
+ ],
"properties": {
"shape": {
"type": "string",
- "enum": ["rectangular"]
+ "enum": ["cuboidal"]
},
- "xDimension": {
+ "bottomXDimension": {
"type": "number"
},
- "yDimension": {
+ "bottomYDimension": {
+ "type": "number"
+ },
+ "topXDimension": {
+ "type": "number"
+ },
+ "topYDimension": {
"type": "number"
},
"topHeight": {
- "type": "number",
- "description": "The height at the top of a bounded subsection of a well, relative to the bottom"
+ "type": "number"
+ },
+ "bottomHeight": {
+ "type": "number"
+ }
+ }
+ },
+ "SquaredConeSegment": {
+ "type": "object",
+ "description": "The intersection of a pyramid and a cone that both share a central axis where one face is a circle and one face is a rectangle",
+ "required": [
+ "shape",
+ "bottomCrossSection",
+ "circleDiameter",
+ "rectangleXDimension",
+ "rectangleYDimension",
+ "topHeight",
+ "bottomHeight"
+ ],
+ "properties": {
+ "shape": {
+ "type": "string",
+ "enum": ["squaredcone"]
+ },
+ "bottomCrossSection": {
+ "type": "string",
+ "enum": ["circular", "rectangular"]
+ },
+ "circleDiameter": {
+ "type": "number"
+ },
+ "rectangleXDimension": {
+ "type": "number"
+ },
+ "rectangleYDimension": {
+ "type": "number"
+ },
+ "topHeight": {
+ "type": "number"
+ },
+ "bottomHeight": {
+ "type": "number"
+ }
+ }
+ },
+ "RoundedCuboidSegment": {
+ "type": "object",
+ "description": "A cuboidal frustum where each corner is filleted out by circles with centers on the diagonals between opposite corners",
+ "required": [
+ "shape",
+ "bottomCrossSection",
+ "circleDiameter",
+ "rectangleXDimension",
+ "rectangleYDimension",
+ "topHeight",
+ "bottomHeight"
+ ],
+ "properties": {
+ "shape": {
+ "type": "string",
+ "enum": ["roundedcuboid"]
+ },
+ "bottomCrossSection": {
+ "type": "string",
+ "enum": ["circular", "rectangular"]
+ },
+ "circleDiameter": {
+ "type": "number"
+ },
+ "rectangleXDimension": {
+ "type": "number"
+ },
+ "rectangleYDimension": {
+ "type": "number"
+ },
+ "topHeight": {
+ "type": "number"
+ },
+ "bottomHeight": {
+ "type": "number"
}
}
},
"InnerWellGeometry": {
"type": "object",
- "required": ["frusta"],
+ "required": ["sections"],
"properties": {
- "frusta": {
+ "sections": {
"description": "A list of all of the sections of the well that have a contiguous shape",
"type": "array",
"items": {
"oneOf": [
{
- "$ref": "#/definitions/CircularBoundedSection"
+ "$ref": "#/definitions/ConicalFrustum"
+ },
+ {
+ "$ref": "#/definitions/CuboidalFrustum"
+ },
+ {
+ "$ref": "#/definitions/SquaredConeSegment"
},
{
- "$ref": "#/definitions/RectangularBoundedSection"
+ "$ref": "#/definitions/RoundedCuboidSegment"
+ },
+ {
+ "$ref": "#/definitions/SphericalSegment"
}
]
}
- },
- "bottomShape": {
- "type": "object",
- "description": "The shape at the bottom of the well: either a spherical segment or a cross-section",
- "$ref": "#/definitions/SphericalSegment"
}
}
}
diff --git a/shared-data/python/opentrons_shared_data/labware/constants.py b/shared-data/python/opentrons_shared_data/labware/constants.py
index 00fbef3c160..9973604937b 100644
--- a/shared-data/python/opentrons_shared_data/labware/constants.py
+++ b/shared-data/python/opentrons_shared_data/labware/constants.py
@@ -1,7 +1,20 @@
import re
from typing_extensions import Final
+from typing import Literal, Union
# Regular expression to validate and extract row, column from well name
# (ie A3, C1)
WELL_NAME_PATTERN: Final["re.Pattern[str]"] = re.compile(r"^([A-Z]+)([0-9]+)$", re.X)
+
+# These shapes are for wellshape definitions and describe the top of the well
+Circular = Literal["circular"]
+Rectangular = Literal["rectangular"]
+WellShape = Union[Circular, Rectangular]
+
+# These shapes are used to describe the 3D primatives used to build wells
+Conical = Literal["conical"]
+Cuboidal = Literal["cuboidal"]
+SquaredCone = Literal["squaredcone"]
+RoundedCuboid = Literal["roundedcuboid"]
+Spherical = Literal["spherical"]
diff --git a/shared-data/python/opentrons_shared_data/labware/labware_definition.py b/shared-data/python/opentrons_shared_data/labware/labware_definition.py
index a6ee1804cde..a818afc106a 100644
--- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py
+++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py
@@ -19,6 +19,15 @@
)
from typing_extensions import Literal
+from .constants import (
+ Conical,
+ Cuboidal,
+ RoundedCuboid,
+ SquaredCone,
+ Spherical,
+ WellShape,
+)
+
SAFE_STRING_REGEX = "^[a-z0-9._]+$"
@@ -228,45 +237,227 @@ class Config:
class SphericalSegment(BaseModel):
- shape: Literal["spherical"] = Field(..., description="Denote shape as spherical")
+ shape: Spherical = Field(..., description="Denote shape as spherical")
radiusOfCurvature: _NonNegativeNumber = Field(
...,
description="radius of curvature of bottom subsection of wells",
)
- depth: _NonNegativeNumber = Field(
+ topHeight: _NonNegativeNumber = Field(
..., description="The depth of a spherical bottom of a well"
)
+ bottomHeight: _NonNegativeNumber = Field(
+ ...,
+ description="Height of the bottom of the segment, must be 0.0",
+ )
+
+
+class ConicalFrustum(BaseModel):
+ shape: Conical = Field(..., description="Denote shape as conical")
+ bottomDiameter: _NonNegativeNumber = Field(
+ ...,
+ description="The diameter at the bottom cross-section of a circular frustum",
+ )
+ topDiameter: _NonNegativeNumber = Field(
+ ..., description="The diameter at the top cross-section of a circular frustum"
+ )
+ topHeight: _NonNegativeNumber = Field(
+ ...,
+ description="The height at the top of a bounded subsection of a well, relative to the bottom"
+ "of the well",
+ )
+ bottomHeight: _NonNegativeNumber = Field(
+ ...,
+ description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well",
+ )
+
+
+class CuboidalFrustum(BaseModel):
+ shape: Cuboidal = Field(..., description="Denote shape as cuboidal")
+ bottomXDimension: _NonNegativeNumber = Field(
+ ...,
+ description="x dimension of the bottom cross-section of a rectangular frustum",
+ )
+ bottomYDimension: _NonNegativeNumber = Field(
+ ...,
+ description="y dimension of the bottom cross-section of a rectangular frustum",
+ )
+ topXDimension: _NonNegativeNumber = Field(
+ ...,
+ description="x dimension of the top cross-section of a rectangular frustum",
+ )
+ topYDimension: _NonNegativeNumber = Field(
+ ...,
+ description="y dimension of the top cross-section of a rectangular frustum",
+ )
+ topHeight: _NonNegativeNumber = Field(
+ ...,
+ description="The height at the top of a bounded subsection of a well, relative to the bottom"
+ "of the well",
+ )
+ bottomHeight: _NonNegativeNumber = Field(
+ ...,
+ description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well",
+ )
+
+
+# A squared cone is the intersection of a cube and a cone that both
+# share a central axis, and is a transitional shape between a cone and pyramid
+"""
+module RectangularPrismToCone(bottom_shape, diameter, x, y, z) {
+ circle_radius = diameter/2;
+ r1 = sqrt(x*x + y*y)/2;
+ r2 = circle_radius/2;
+ top_r = bottom_shape == "square" ? r1 : r2;
+ bottom_r = bottom_shape == "square" ? r2 : r1;
+ intersection() {
+ cylinder(z,top_r,bottom_r,$fn=100);
+ translate([0,0,z/2])cube([x, y, z], center=true);
+ }
+}
+"""
-class CircularBoundedSection(BaseModel):
- shape: Literal["circular"] = Field(..., description="Denote shape as circular")
- diameter: _NonNegativeNumber = Field(
- ..., description="The diameter of a circular cross section of a well"
+class SquaredConeSegment(BaseModel):
+ shape: SquaredCone = Field(
+ ..., description="Denote shape as a squared conical segment"
+ )
+ bottomCrossSection: WellShape = Field(
+ ...,
+ description="Denote if the shape is going from circular to rectangular or vise versa",
+ )
+ circleDiameter: _NonNegativeNumber = Field(
+ ...,
+ description="diameter of the circular face of a truncated circular segment",
+ )
+
+ rectangleXDimension: _NonNegativeNumber = Field(
+ ...,
+ description="x dimension of the rectangular face of a truncated circular segment",
+ )
+ rectangleYDimension: _NonNegativeNumber = Field(
+ ...,
+ description="y dimension of the rectangular face of a truncated circular segment",
)
topHeight: _NonNegativeNumber = Field(
...,
description="The height at the top of a bounded subsection of a well, relative to the bottom"
"of the well",
)
+ bottomHeight: _NonNegativeNumber = Field(
+ ...,
+ description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well",
+ )
-class RectangularBoundedSection(BaseModel):
- shape: Literal["rectangular"] = Field(
- ..., description="Denote shape as rectangular"
+"""
+module filitedCuboidSquare(bottom_shape, diameter, width, length, height, steps) {
+ module _slice(depth, x, y, r) {
+ echo("called with: ", depth, x, y, r);
+ circle_centers = [
+ [(x/2)-r, (y/2)-r, 0],
+ [(-x/2)+r, (y/2)-r, 0],
+ [(x/2)-r, (-y/2)+r, 0],
+ [(-x/2)+r, (-y/2)+r, 0]
+
+ ];
+ translate([0,0,depth/2])cube([x-2*r,y,depth], center=true);
+ translate([0,0,depth/2])cube([x,y-2*r,depth], center=true);
+ for (center = circle_centers) {
+ translate(center) cylinder(depth, r, r, $fn=100);
+ }
+ }
+ for (slice_height = [0:height/steps:height]) {
+ r = (diameter) * (slice_height/height);
+ translate([0,0,slice_height]) {
+ _slice(height/steps , width, length, r/2);
+ }
+ }
+}
+module filitedCuboidForce(bottom_shape, diameter, width, length, height, steps) {
+ module single_cone(r,x,y,z) {
+ r = diameter/2;
+ circle_face = [[ for (i = [0:1: steps]) i ]];
+ theta = 360/steps;
+ circle_points = [for (step = [0:1:steps]) [r*cos(theta*step), r*sin(theta*step), z]];
+ final_points = [[x,y,0]];
+ all_points = concat(circle_points, final_points);
+ triangles = [for (step = [0:1:steps-1]) [step, step+1, steps+1]];
+ faces = concat(circle_face, triangles);
+ polyhedron(all_points, faces);
+ }
+ module square_section(r, x, y, z) {
+ points = [
+ [x,y,0],
+ [-x,y,0],
+ [-x,-y,0],
+ [x,-y,0],
+ [r,0,z],
+ [0,r,z],
+ [-r,0,z],
+ [0,-r,z],
+ ];
+ faces = [
+ [0,1,2,3],
+ [4,5,6,7],
+ [4, 0, 3],
+ [5, 0, 1],
+ [6, 1, 2],
+ [7, 2, 3],
+ ];
+ polyhedron(points, faces);
+ }
+ circle_height = bottom_shape == "square" ? height : -height;
+ translate_height = bottom_shape == "square" ? 0 : height;
+ translate ([0,0, translate_height]) {
+ union() {
+ single_cone(diameter/2, width/2, length/2, circle_height);
+ single_cone(diameter/2, -width/2, length/2, circle_height);
+ single_cone(diameter/2, width/2, -length/2, circle_height);
+ single_cone(diameter/2, -width/2, -length/2, circle_height);
+ square_section(diameter/2, width/2, length/2, circle_height);
+ }
+ }
+}
+
+module filitedCuboid(bottom_shape, diameter, width, length, height) {
+ if (width == length && width == diameter) {
+ filitedCuboidSquare(bottom_shape, diameter, width, length, height, 100);
+ }
+ else {
+ filitedCuboidForce(bottom_shape, diameter, width, length, height, 100);
+ }
+}"""
+
+
+class RoundedCuboidSegment(BaseModel):
+ shape: RoundedCuboid = Field(
+ ..., description="Denote shape as a rounded cuboidal segment"
+ )
+ bottomCrossSection: WellShape = Field(
+ ...,
+ description="Denote if the shape is going from circular to rectangular or vise versa",
+ )
+ circleDiameter: _NonNegativeNumber = Field(
+ ...,
+ description="diameter of the circular face of a rounded rectangular segment",
)
- xDimension: _NonNegativeNumber = Field(
+ rectangleXDimension: _NonNegativeNumber = Field(
...,
- description="x dimension of a subsection of wells",
+ description="x dimension of the rectangular face of a rounded rectangular segment",
)
- yDimension: _NonNegativeNumber = Field(
+ rectangleYDimension: _NonNegativeNumber = Field(
...,
- description="y dimension of a subsection of wells",
+ description="y dimension of the rectangular face of a rounded rectangular segment",
)
topHeight: _NonNegativeNumber = Field(
...,
description="The height at the top of a bounded subsection of a well, relative to the bottom"
"of the well",
)
+ bottomHeight: _NonNegativeNumber = Field(
+ ...,
+ description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well",
+ )
class Metadata1(BaseModel):
@@ -297,17 +488,20 @@ class Group(BaseModel):
)
+WellSegment = Union[
+ ConicalFrustum,
+ CuboidalFrustum,
+ SquaredConeSegment,
+ RoundedCuboidSegment,
+ SphericalSegment,
+]
+
+
class InnerWellGeometry(BaseModel):
- frusta: Union[
- List[CircularBoundedSection], List[RectangularBoundedSection]
- ] = Field(
+ sections: List[WellSegment] = Field(
...,
description="A list of all of the sections of the well that have a contiguous shape",
)
- bottomShape: Optional[SphericalSegment] = Field(
- None,
- description="The shape at the bottom of the well: either a spherical segment or a cross-section",
- )
class LabwareDefinition(BaseModel):
diff --git a/shared-data/python/opentrons_shared_data/labware/types.py b/shared-data/python/opentrons_shared_data/labware/types.py
index 9ea7a83fb6b..d3f6599848c 100644
--- a/shared-data/python/opentrons_shared_data/labware/types.py
+++ b/shared-data/python/opentrons_shared_data/labware/types.py
@@ -3,9 +3,13 @@
types in this file by and large require the use of typing_extensions.
this module shouldn't be imported unless typing.TYPE_CHECKING is true.
"""
-from typing import Dict, List, NewType, Union, Optional, Any
-from typing_extensions import Literal, TypedDict, NotRequired, TypeGuard
-
+from typing import Dict, List, NewType, Union
+from typing_extensions import Literal, TypedDict, NotRequired
+from .labware_definition import InnerWellGeometry
+from .constants import (
+ Circular,
+ Rectangular,
+)
LabwareUri = NewType("LabwareUri", str)
@@ -35,11 +39,6 @@
Literal["maintenance"],
]
-Circular = Literal["circular"]
-Rectangular = Literal["rectangular"]
-Spherical = Literal["spherical"]
-WellShape = Union[Circular, Rectangular]
-
class NamedOffset(TypedDict):
x: float
@@ -120,42 +119,6 @@ class WellGroup(TypedDict, total=False):
brand: LabwareBrandData
-class SphericalSegment(TypedDict):
- shape: Spherical
- radiusOfCurvature: float
- depth: float
-
-
-class RectangularBoundedSection(TypedDict):
- shape: Rectangular
- xDimension: float
- yDimension: float
- topHeight: float
-
-
-class CircularBoundedSection(TypedDict):
- shape: Circular
- diameter: float
- topHeight: float
-
-
-def is_circular_frusta_list(
- items: List[Any],
-) -> TypeGuard[List[CircularBoundedSection]]:
- return all(item.shape == "circular" for item in items)
-
-
-def is_rectangular_frusta_list(
- items: List[Any],
-) -> TypeGuard[List[RectangularBoundedSection]]:
- return all(item.shape == "rectangular" for item in items)
-
-
-class InnerWellGeometry(TypedDict):
- frusta: Union[List[CircularBoundedSection], List[RectangularBoundedSection]]
- bottomShape: Optional[SphericalSegment]
-
-
class LabwareDefinition(TypedDict):
schemaVersion: Literal[2]
version: int