Skip to content

Commit

Permalink
refactor(api): Save and load tip length calibrations from tiprack uri
Browse files Browse the repository at this point in the history
  • Loading branch information
Laura-Danielle committed Feb 16, 2024
1 parent 4c4f491 commit ca0baa4
Show file tree
Hide file tree
Showing 8 changed files with 55 additions and 43 deletions.
4 changes: 2 additions & 2 deletions api/src/opentrons/calibration_storage/ot2/models/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ class TipLengthModel(BaseModel):
default_factory=CalibrationStatus,
description="The status of the calibration data.",
)
uri: typing.Union[LabwareUri, Literal[""]] = Field(
..., description="The tiprack URI associated with the tip length data."
definitionHash: str = Field(
..., description="The tiprack hash associated with the tip length data."
)

@validator("tipLength")
Expand Down
43 changes: 24 additions & 19 deletions api/src/opentrons/calibration_storage/ot2/tip_length.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from opentrons import config

from .. import file_operators as io, helpers, types as local_types
from opentrons_shared_data.pipette.dev_types import LabwareUri

from opentrons.protocols.api_support.constants import OPENTRONS_NAMESPACE
from opentrons.util.helpers import utc_now
Expand All @@ -22,9 +23,9 @@
# Get Tip Length Calibration


def _conver_tip_length_model_to_dict(
to_dict: typing.Dict[str, v1.TipLengthModel]
) -> typing.Dict[str, typing.Any]:
def _convert_tip_length_model_to_dict(
to_dict: typing.Dict[LabwareUri, v1.TipLengthModel]
) -> typing.Dict[LabwareUri, typing.Any]:
# This is a workaround since pydantic doesn't have a nice way to
# add encoders when converting to a dict.
dict_of_tip_lengths = {}
Expand All @@ -35,17 +36,21 @@ def _conver_tip_length_model_to_dict(

def tip_lengths_for_pipette(
pipette_id: str,
) -> typing.Dict[str, v1.TipLengthModel]:
) -> typing.Dict[LabwareUri, v1.TipLengthModel]:
tip_lengths = {}
try:
tip_length_filepath = config.get_tip_length_cal_path() / f"{pipette_id}.json"
all_tip_lengths_for_pipette = io.read_cal_file(tip_length_filepath)
for tiprack, data in all_tip_lengths_for_pipette.items():
for tiprack_uri, data in all_tip_lengths_for_pipette.items():
# check if tiprack hash
if len(tiprack_uri.split("/")) == 1:
data["definitionHash"] = tiprack_uri
tiprack_uri = data.pop("uri")
try:
tip_lengths[tiprack] = v1.TipLengthModel(**data)
tip_lengths[LabwareUri(tiprack_uri)] = v1.TipLengthModel(**data)
except (json.JSONDecodeError, ValidationError):
log.warning(
f"Tip length calibration is malformed for {tiprack} on {pipette_id}"
f"Tip length calibration is malformed for {tiprack_uri} on {pipette_id}"
)
pass
return tip_lengths
Expand All @@ -64,10 +69,10 @@ def load_tip_length_calibration(
:param pip_id: pipette you are using
:param definition: full definition of the tiprack
"""
labware_hash = helpers.hash_labware_def(definition)
labware_uri = helpers.uri_from_definition(definition)
load_name = definition["parameters"]["loadName"]
try:
return tip_lengths_for_pipette(pip_id)[labware_hash]
return tip_lengths_for_pipette(pip_id)[labware_uri]
except KeyError as e:
raise local_types.TipLengthCalNotFound(
f"Tip length of {load_name} has not been "
Expand All @@ -89,16 +94,16 @@ def get_all_tip_length_calibrations() -> typing.List[v1.TipLengthCalibration]:
if filepath.stem == "index":
continue
tip_lengths = tip_lengths_for_pipette(filepath.stem)
for tiprack_hash, tip_length in tip_lengths.items():
for tiprack_uri, tip_length in tip_lengths.items():
all_tip_lengths_available.append(
v1.TipLengthCalibration(
pipette=filepath.stem,
tiprack=tiprack_hash,
tiprack=tip_length.definitionHash,
tipLength=tip_length.tipLength,
lastModified=tip_length.lastModified,
source=tip_length.source,
status=tip_length.status,
uri=tip_length.uri,
uri=tiprack_uri,
)
)
return all_tip_lengths_available
Expand Down Expand Up @@ -129,7 +134,7 @@ def get_custom_tiprack_definition_for_tlc(labware_uri: str) -> "LabwareDefinitio
# Delete Tip Length Calibration


def delete_tip_length_calibration(tiprack: str, pipette_id: str) -> None:
def delete_tip_length_calibration(tiprack: LabwareUri, pipette_id: str) -> None:
"""
Delete tip length calibration based on tiprack hash and
pipette serial number
Expand All @@ -144,7 +149,7 @@ def delete_tip_length_calibration(tiprack: str, pipette_id: str) -> None:
del tip_lengths[tiprack]
tip_length_dir = config.get_tip_length_cal_path()
if tip_lengths:
dict_of_tip_lengths = _conver_tip_length_model_to_dict(tip_lengths)
dict_of_tip_lengths = _convert_tip_length_model_to_dict(tip_lengths)
io.save_to_file(tip_length_dir, pipette_id, dict_of_tip_lengths)
else:
io.delete_file(tip_length_dir / f"{pipette_id}.json")
Expand Down Expand Up @@ -176,7 +181,7 @@ def create_tip_length_data(
cal_status: typing.Optional[
typing.Union[local_types.CalibrationStatus, v1.CalibrationStatus]
] = None,
) -> typing.Dict[str, v1.TipLengthModel]:
) -> typing.Dict[LabwareUri, v1.TipLengthModel]:
"""
Function to correctly format tip length data.
Expand All @@ -197,13 +202,13 @@ def create_tip_length_data(
lastModified=utc_now(),
source=local_types.SourceType.user,
status=cal_status_model,
uri=labware_uri,
definitionHash=labware_hash,
)

if not definition.get("namespace") == OPENTRONS_NAMESPACE:
_save_custom_tiprack_definition(labware_uri, definition)

data = {labware_hash: tip_length_data}
data = {labware_uri: tip_length_data}
return data


Expand All @@ -220,7 +225,7 @@ def _save_custom_tiprack_definition(

def save_tip_length_calibration(
pip_id: str,
tip_length_cal: typing.Dict[str, v1.TipLengthModel],
tip_length_cal: typing.Dict[LabwareUri, v1.TipLengthModel],
) -> None:
"""
Function used to save tip length calibration to file.
Expand All @@ -235,5 +240,5 @@ def save_tip_length_calibration(

all_tip_lengths.update(tip_length_cal)

dict_of_tip_lengths = _conver_tip_length_model_to_dict(all_tip_lengths)
dict_of_tip_lengths = _convert_tip_length_model_to_dict(all_tip_lengths)
io.save_to_file(tip_length_dir_path, pip_id, dict_of_tip_lengths)
Original file line number Diff line number Diff line change
Expand Up @@ -133,15 +133,15 @@ def load_tip_length_for_pipette(
# TODO (lc 09-26-2022) We shouldn't have to do a hash twice. We should figure out what
# information we actually need from the labware definition and pass it into
# the `load_tip_length_calibration` function.
tiprack_hash = helpers.hash_labware_def(tiprack)
tiprack_uri = helpers.uri_from_definition(tiprack)

return TipLengthCalibration(
tip_length=tip_length_data.tipLength,
source=tip_length_data.source,
pipette=pipette_id,
tiprack=tiprack_hash,
tiprack=tip_length_data.definitionHash,
last_modified=tip_length_data.lastModified,
uri=tip_length_data.uri,
uri=tiprack_uri,
status=types.CalibrationStatus(
markedAt=tip_length_data.status.markedAt,
markedBad=tip_length_data.status.markedBad,
Expand Down
16 changes: 8 additions & 8 deletions api/tests/opentrons/calibration_storage/test_tip_length_ot2.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pytest
from typing import cast, Any, TYPE_CHECKING
from typing import Any, TYPE_CHECKING

from opentrons.calibration_storage import (
types as cs_types,
Expand All @@ -18,7 +18,6 @@

if TYPE_CHECKING:
from opentrons_shared_data.labware.dev_types import LabwareDefinition
from opentrons_shared_data.pipette.dev_types import LabwareUri


@pytest.fixture
Expand Down Expand Up @@ -48,13 +47,13 @@ def test_save_tip_length_calibration(
"""
assert tip_lengths_for_pipette("pip1") == {}
assert tip_lengths_for_pipette("pip2") == {}
tip_rack_hash = helpers.hash_labware_def(minimal_labware_def)
tip_rack_uri = helpers.uri_from_definition(minimal_labware_def)
tip_length1 = create_tip_length_data(minimal_labware_def, 22.0)
tip_length2 = create_tip_length_data(minimal_labware_def, 31.0)
save_tip_length_calibration("pip1", tip_length1)
save_tip_length_calibration("pip2", tip_length2)
assert tip_lengths_for_pipette("pip1")[tip_rack_hash].tipLength == 22.0
assert tip_lengths_for_pipette("pip2")[tip_rack_hash].tipLength == 31.0
assert tip_lengths_for_pipette("pip1")[tip_rack_uri].tipLength == 22.0
assert tip_lengths_for_pipette("pip2")[tip_rack_uri].tipLength == 31.0


def test_get_tip_length_calibration(
Expand All @@ -64,11 +63,12 @@ def test_get_tip_length_calibration(
Test ability to get a tip length calibration model.
"""
tip_length_data = load_tip_length_calibration("pip1", minimal_labware_def)
tip_rack_hash = helpers.hash_labware_def(minimal_labware_def)
assert tip_length_data == models.v1.TipLengthModel(
tipLength=22.0,
source=cs_types.SourceType.user,
lastModified=tip_length_data.lastModified,
uri=cast("LabwareUri", "opentronstest/minimal_labware_def/1"),
definitionHash=tip_rack_hash,
)

with pytest.raises(cs_types.TipLengthCalNotFound):
Expand All @@ -83,8 +83,8 @@ def test_delete_specific_tip_calibration(
"""
assert len(tip_lengths_for_pipette("pip1").keys()) == 2
assert tip_lengths_for_pipette("pip2") != {}
tip_rack_hash = helpers.hash_labware_def(minimal_labware_def)
delete_tip_length_calibration(tip_rack_hash, "pip1")
tip_rack_uri = helpers.uri_from_definition(minimal_labware_def)
delete_tip_length_calibration(tip_rack_uri, "pip1")
assert len(tip_lengths_for_pipette("pip1").keys()) == 1
assert tip_lengths_for_pipette("pip2") != {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def test_load_tip_length(
tip_length_data = v1_models.TipLengthModel(
tipLength=1.23,
lastModified=datetime(year=2023, month=1, day=1),
uri=LabwareUri("def456"),
definitionHash="asdfghjk",
source=subject.SourceType.factory,
status=v1_models.CalibrationStatus(
markedBad=True,
Expand All @@ -99,6 +99,9 @@ def test_load_tip_length(
decoy.when(calibration_storage.helpers.hash_labware_def(tip_rack_dict)).then_return(
"asdfghjk"
)
decoy.when(calibration_storage.helpers.uri_from_definition(tip_rack_dict)).then_return(
"def456"
)

result = subject.load_tip_length_for_pipette(
pipette_id="abc123", tiprack=tip_rack_definition
Expand Down
13 changes: 8 additions & 5 deletions robot-server/robot_server/service/tip_length/router.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from starlette import status
from fastapi import APIRouter, Depends
from typing import Optional
from typing import Optional, cast

from opentrons.calibration_storage import types as cal_types
from opentrons.calibration_storage.ot2 import tip_length, models
Expand All @@ -12,6 +12,7 @@
from robot_server.service.shared_models import calibration as cal_model

from opentrons.hardware_control import API
from opentrons_shared_data.pipette.dev_types import LabwareUri


router = APIRouter()
Expand Down Expand Up @@ -80,17 +81,19 @@ async def get_all_tip_length_calibrations(
@router.delete(
"/calibration/tip_length",
description="Delete one specific tip length calibration by pipette "
"serial and tiprack hash",
"serial and tiprack uri",
responses={status.HTTP_404_NOT_FOUND: {"model": ErrorBody}},
)
async def delete_specific_tip_length_calibration(
tiprack_hash: str, pipette_id: str, _: API = Depends(get_ot2_hardware)
tiprack_uri: str, pipette_id: str, _: API = Depends(get_ot2_hardware)
):
try:
tip_length.delete_tip_length_calibration(tiprack_hash, pipette_id)
tip_length.delete_tip_length_calibration(
cast(LabwareUri, tiprack_uri), pipette_id
)
except cal_types.TipLengthCalNotFound:
raise RobotServerError(
definition=CommonErrorDef.RESOURCE_NOT_FOUND,
resource="TipLengthCalibration",
id=f"{tiprack_hash}&{pipette_id}",
id=f"{tiprack_uri}&{pipette_id}",
)
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,14 @@ marks: *cal_marks
stages:
- name: DELETE request with correct pipette AND tiprack
request:
url: "{ot2_server_base_url}/calibration/tip_length?pipette_id=321&tiprack_hash=130e17bb7b2f0c0472dcc01c1ff6f600ca1a6f9f86a90982df56c4bf43776824"
url: "{ot2_server_base_url}/calibration/tip_length?pipette_id=321&tiprack_uri=opentrons/opentrons_96_filtertiprack_200ul/1"
method: DELETE
response:
status_code: 200

- name: DELETE request with incorrect pipette AND tiprack
request:
url: "{ot2_server_base_url}/calibration/tip_length?pipette_id=321&tiprack_hash=wronghash"
url: "{ot2_server_base_url}/calibration/tip_length?pipette_id=321&tiprack_uri=wronguri"
method: DELETE
response:
status_code: 404
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
PIPETTE_ID = "123"
LW_HASH = "130e17bb7b2f0c0472dcc01c1ff6f600ca1a6f9f86a90982df56c4bf43776824"
LW_URI = "opentrons/opentrons_96_filtertiprack_200ul/1"
FAKE_PIPETTE_ID = "fake_pip"
WRONG_LW_HASH = "wronghash"

Expand Down Expand Up @@ -33,11 +34,11 @@ def test_access_tip_length_calibration(api_client, set_up_tip_length_temp_direct


def test_delete_tip_length_calibration(
api_client, set_up_pipette_offset_temp_directory
api_client, set_up_tip_length_temp_directory
):
resp = api_client.delete(
f"/calibration/tip_length?pipette_id={FAKE_PIPETTE_ID}&"
f"tiprack_hash={WRONG_LW_HASH}"
f"tiprack_uri={WRONG_LW_HASH}"
)
assert resp.status_code == 404
body = resp.json()
Expand All @@ -54,6 +55,6 @@ def test_delete_tip_length_calibration(
}

resp = api_client.delete(
f"/calibration/tip_length?pipette_id={PIPETTE_ID}&" f"tiprack_hash={LW_HASH}"
f"/calibration/tip_length?pipette_id={PIPETTE_ID}&" f"tiprack_uri={LW_URI}"
)
assert resp.status_code == 200

0 comments on commit ca0baa4

Please sign in to comment.