Skip to content

Commit

Permalink
refactor(shared-data): Split TypedDict-based bindings for labware s…
Browse files Browse the repository at this point in the history
…chemas 2 and 3 (#17517)
  • Loading branch information
SyntaxColoring authored Feb 14, 2025
1 parent 832d939 commit 41e0f6f
Show file tree
Hide file tree
Showing 44 changed files with 376 additions and 206 deletions.
12 changes: 6 additions & 6 deletions api/src/opentrons/calibration_storage/ot2/tip_length.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from .models import v1

if typing.TYPE_CHECKING:
from opentrons_shared_data.labware.types import LabwareDefinition
from opentrons_shared_data.labware.types import LabwareDefinition2


log = logging.getLogger(__name__)
Expand Down Expand Up @@ -78,7 +78,7 @@ def tip_lengths_for_pipette(


def load_tip_length_calibration(
pip_id: str, definition: "LabwareDefinition"
pip_id: str, definition: "LabwareDefinition2"
) -> v1.TipLengthModel:
"""
Function used to grab the current tip length associated
Expand Down Expand Up @@ -127,7 +127,7 @@ def get_all_tip_length_calibrations() -> typing.List[v1.TipLengthCalibration]:
return all_tip_lengths_available


def get_custom_tiprack_definition_for_tlc(labware_uri: str) -> "LabwareDefinition":
def get_custom_tiprack_definition_for_tlc(labware_uri: str) -> "LabwareDefinition2":
"""
Return the custom tiprack definition saved in the custom tiprack directory
during tip length calibration
Expand All @@ -137,7 +137,7 @@ def get_custom_tiprack_definition_for_tlc(labware_uri: str) -> "LabwareDefinitio
try:
with open(custom_tiprack_path, "rb") as f:
return typing.cast(
"LabwareDefinition",
"LabwareDefinition2",
json.loads(f.read().decode("utf-8")),
)
except FileNotFoundError:
Expand Down Expand Up @@ -213,7 +213,7 @@ def clear_tip_length_calibration() -> None:


def create_tip_length_data(
definition: "LabwareDefinition",
definition: "LabwareDefinition2",
length: float,
cal_status: typing.Optional[
typing.Union[local_types.CalibrationStatus, v1.CalibrationStatus]
Expand Down Expand Up @@ -251,7 +251,7 @@ def create_tip_length_data(

def _save_custom_tiprack_definition(
labware_uri: str,
definition: "LabwareDefinition",
definition: "LabwareDefinition2",
) -> None:
namespace, load_name, version = labware_uri.split("/")
custom_tr_dir_path = config.get_custom_tiprack_def_path()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
if typing.TYPE_CHECKING:
from opentrons_shared_data.pipette.types import LabwareUri
from opentrons_shared_data.labware.types import (
LabwareDefinition as TypeDictLabwareDef,
LabwareDefinition2 as TypeDictLabwareDef2,
)

# These type aliases aid typechecking in tests that work the same on this and
Expand Down Expand Up @@ -119,12 +119,19 @@ def save_pipette_offset_calibration(
# TODO (lc 09-26-2022) We should ensure that only LabwareDefinition models are passed
# into this function instead of a mixture of TypeDicts and BaseModels
def load_tip_length_for_pipette(
pipette_id: str, tiprack: typing.Union["TypeDictLabwareDef", LabwareDefinition]
pipette_id: str, tiprack: typing.Union["TypeDictLabwareDef2", LabwareDefinition]
) -> TipLengthCalibration:
if isinstance(tiprack, LabwareDefinition):
# todo(mm, 2025-02-13): This is only correct for schema 2 labware.
# The LabwareDefinition union member needs to be narrowed to LabwareDefinition2,
# which doesn't exist yet (https://opentrons.atlassian.net/browse/EXEC-1206).
tiprack = typing.cast(
"TypeDictLabwareDef",
tiprack.model_dump(exclude_none=True, exclude_unset=True),
"TypeDictLabwareDef2",
tiprack.model_dump(
exclude_none=True,
exclude_unset=True
# todo(mm, 2025-02-13): Do we need by_alias=True here?
),
)

tip_length_data = calibration_storage.load_tip_length_calibration(
Expand Down
10 changes: 7 additions & 3 deletions api/src/opentrons/protocol_api/core/engine/labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from typing import List, Optional, cast, Dict

from opentrons_shared_data.labware.types import (
LabwareParameters as LabwareParametersDict,
LabwareParameters2 as LabwareParameters2Dict,
LabwareParameters3 as LabwareParameters3Dict,
LabwareDefinition as LabwareDefinitionDict,
)

Expand All @@ -29,6 +30,9 @@
from .well import WellCore


_LabwareParametersDict = LabwareParameters2Dict | LabwareParameters3Dict


class LabwareCore(AbstractLabware[WellCore]):
"""Labware API core using a ProtocolEngine.
Expand Down Expand Up @@ -96,9 +100,9 @@ def get_definition(self) -> LabwareDefinitionDict:
LabwareDefinitionDict, self._definition.model_dump(exclude_none=True)
)

def get_parameters(self) -> LabwareParametersDict:
def get_parameters(self) -> _LabwareParametersDict:
return cast(
LabwareParametersDict,
_LabwareParametersDict,
self._definition.parameters.model_dump(exclude_none=True),
)

Expand Down
8 changes: 6 additions & 2 deletions api/src/opentrons/protocol_api/core/labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

from opentrons_shared_data.labware.types import (
LabwareUri,
LabwareParameters as LabwareParametersDict,
LabwareParameters2,
LabwareParameters3,
LabwareDefinition as LabwareDefinitionDict,
)

Expand All @@ -17,6 +18,9 @@
from .well import WellCoreType


_LabwareParametersDict = LabwareParameters2 | LabwareParameters3


class LabwareLoadParams(NamedTuple):
"""Unique load parameters of a labware."""

Expand Down Expand Up @@ -75,7 +79,7 @@ def get_definition(self) -> LabwareDefinitionDict:
"""Get the labware's definition as a plain dictionary."""

@abstractmethod
def get_parameters(self) -> LabwareParametersDict:
def get_parameters(self) -> _LabwareParametersDict:
"""Get the labware's definition's `parameters` field as a plain dictionary."""

@abstractmethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from opentrons.types import DeckSlotName, Location, Point, NozzleMapInterface

from opentrons_shared_data.labware.types import LabwareParameters, LabwareDefinition
from opentrons_shared_data.labware.types import LabwareParameters2, LabwareDefinition2

from ..._liquid import Liquid
from ..labware import AbstractLabware, LabwareLoadParams
Expand Down Expand Up @@ -36,7 +36,11 @@ class LegacyLabwareCore(AbstractLabware[LegacyWellCore]):

def __init__(
self,
definition: LabwareDefinition,
# We need labware schema 2, specifically, because schema 3 changes how positions
# are calculated, and we don't attempt to handle that here in
# `opentrons.protocol_api.core.legacy`. We do handle it in
# `opentrons.protocol_api.core.engine` and `opentrons.protocol_engine`.
definition: LabwareDefinition2,
parent: Location,
label: Optional[str] = None,
) -> None:
Expand Down Expand Up @@ -106,10 +110,10 @@ def get_name(self) -> str:
def set_name(self, new_name: str) -> None:
self._name = new_name

def get_definition(self) -> LabwareDefinition:
def get_definition(self) -> LabwareDefinition2:
return self._definition

def get_parameters(self) -> LabwareParameters:
def get_parameters(self) -> LabwareParameters2:
return self._parameters

def get_quirks(self) -> List[str]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@ def load_labware(
bundled_defs=self._bundled_labware,
extra_defs=self._extra_labware,
)
# For type checking. This should always pass because
# opentrons.protocol_api.core.legacy should only load labware with schema 2.
assert labware_def["schemaVersion"] == 2

labware_core = LegacyLabwareCore(
definition=labware_def,
parent=parent,
Expand Down
16 changes: 4 additions & 12 deletions api/src/opentrons/protocol_api/core/legacy/load_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from dataclasses import dataclass
from typing import Optional, Union
from opentrons_shared_data.labware.types import LabwareDefinition
from opentrons_shared_data.labware.types import LabwareDefinition2

from opentrons.hardware_control.dev_types import PipetteDict
from opentrons.hardware_control.modules.types import ModuleModel
Expand All @@ -18,13 +18,11 @@
class LabwareLoadInfo:
"""Information about a successful labware load.
:meta private:
This is a separate class from the main user-facing `Labware` class
because this is easier to construct in unit tests.
"""

labware_definition: LabwareDefinition
labware_definition: LabwareDefinition2

# todo(mm, 2021-10-11): Namespace, load name, and version can be derived from the
# definition. Should they be removed from here?
Expand All @@ -47,10 +45,7 @@ class LabwareLoadInfo:

@dataclass(frozen=True)
class InstrumentLoadInfo:
"""Like `LabwareLoadInfo`, but for instruments (pipettes).
:meta private:
"""
"""Like `LabwareLoadInfo`, but for instruments (pipettes)."""

instrument_load_name: str
mount: Mount
Expand All @@ -59,10 +54,7 @@ class InstrumentLoadInfo:

@dataclass(frozen=True)
class ModuleLoadInfo:
"""Like `LabwareLoadInfo`, but for hardware modules.
:meta private:
"""
"""Like `LabwareLoadInfo`, but for hardware modules."""

requested_model: ModuleModel
loaded_model: ModuleModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,13 @@ def labware_accessor(self, labware: Labware) -> Labware:
LegacyLabwareCore,
)

# Block first three columns from being accessed
definition = labware._core.get_definition()

# For type checking. This should always pass because
# opentrons.protocol_api.core.legacy should only load labware with schema 2.
assert definition["schemaVersion"] == 2

# Block first three columns from being accessed
definition["ordering"] = definition["ordering"][2::]
return Labware(
core=LegacyLabwareCore(definition, super().location),
Expand Down
6 changes: 3 additions & 3 deletions api/src/opentrons/protocol_api/core/legacy/well_geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

from opentrons.types import Point
from opentrons_shared_data.labware.types import (
WellDefinition,
CircularWellDefinition,
RectangularWellDefinition,
WellDefinition2 as WellDefinition,
CircularWellDefinition2 as CircularWellDefinition,
RectangularWellDefinition2 as RectangularWellDefinition,
)

if TYPE_CHECKING:
Expand Down
25 changes: 20 additions & 5 deletions api/src/opentrons/protocol_api/labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@
Mapping,
)

from opentrons_shared_data.labware.types import LabwareDefinition, LabwareParameters
from opentrons_shared_data.labware.types import (
LabwareDefinition,
LabwareDefinition2,
LabwareParameters2,
LabwareParameters3,
)

from opentrons.types import Location, Point, NozzleMapInterface
from opentrons.protocols.api_support.types import APIVersion
Expand Down Expand Up @@ -537,7 +542,7 @@ def load_name(self) -> str:

@property
@requires_version(2, 0)
def parameters(self) -> "LabwareParameters":
def parameters(self) -> "LabwareParameters2 | LabwareParameters3":
"""Internal properties of a labware including type and quirks."""
return self._core.get_parameters()

Expand Down Expand Up @@ -1445,7 +1450,7 @@ def next_available_tip(
# TODO(mc, 2022-11-09): implementation detail, move somewhere else
# only used in old calibration flows by robot-server
def load_from_definition(
definition: "LabwareDefinition",
definition: "LabwareDefinition2",
parent: Location,
label: Optional[str] = None,
api_level: Optional[APIVersion] = None,
Expand Down Expand Up @@ -1489,8 +1494,8 @@ def load(
label: Optional[str] = None,
namespace: Optional[str] = None,
version: int = 1,
bundled_defs: Optional[Dict[str, LabwareDefinition]] = None,
extra_defs: Optional[Dict[str, LabwareDefinition]] = None,
bundled_defs: Optional[Mapping[str, LabwareDefinition2]] = None,
extra_defs: Optional[Mapping[str, LabwareDefinition2]] = None,
api_level: Optional[APIVersion] = None,
) -> Labware:
"""
Expand Down Expand Up @@ -1528,4 +1533,14 @@ def load(
extra_defs=extra_defs,
)

# The legacy `load_from_definition()` function that we're calling only supports
# schemaVersion==2 labware. Fortunately, when robot-server calls this function,
# we only expect it to try to load schemaVersion==2 labware, so we never expect
# this ValueError to be raised in practice.
if definition["schemaVersion"] != 2:
raise ValueError(
f"{namespace}/{load_name}/{version} has schema {definition['schemaVersion']}."
" Only schema 2 is supported."
)

return load_from_definition(definition, parent, label, api_level)
4 changes: 2 additions & 2 deletions api/src/opentrons/protocols/api_support/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Optional, Any

from opentrons_shared_data.labware.types import (
LabwareDefinition as LabwareDefinitionDict,
LabwareDefinition2 as LabwareDefinition2Dict,
)

from opentrons import types
Expand Down Expand Up @@ -56,7 +56,7 @@ def validate_blowout_location(


def tip_length_for(
pipette: PipetteDict, tip_rack_definition: LabwareDefinitionDict
pipette: PipetteDict, tip_rack_definition: LabwareDefinition2Dict
) -> float:
"""Get the tip length, including overlap, for a tip from this rack"""
try:
Expand Down
11 changes: 5 additions & 6 deletions api/src/opentrons/protocols/labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import json
import os
from pathlib import Path
from typing import Any, AnyStr, Dict, Optional, Union, List, Sequence, Literal
from typing import Any, AnyStr, Dict, Mapping, Optional, Union, List, Sequence, Literal

import jsonschema # type: ignore

Expand Down Expand Up @@ -46,8 +46,8 @@ def get_labware_definition(
load_name: str,
namespace: Optional[str] = None,
version: Optional[int] = None,
bundled_defs: Optional[Dict[str, LabwareDefinition]] = None,
extra_defs: Optional[Dict[str, LabwareDefinition]] = None,
bundled_defs: Optional[Mapping[str, LabwareDefinition]] = None,
extra_defs: Optional[Mapping[str, LabwareDefinition]] = None,
) -> LabwareDefinition:
"""
Look up and return a definition by load_name + namespace + version and
Expand Down Expand Up @@ -154,8 +154,7 @@ def verify_definition( # noqa: C901
If the definition is invalid, an exception is raised; otherwise parse the
json and return the valid definition.
:raises json.JsonDecodeError: If the definition is not valid json
:raises jsonschema.ValidationError: If the definition is not valid.
:raises NotALabwareError:
:returns: The parsed definition
"""
schemata_by_version = {
Expand Down Expand Up @@ -191,7 +190,7 @@ def verify_definition( # noqa: C901


def _get_labware_definition_from_bundle(
bundled_labware: Dict[str, LabwareDefinition],
bundled_labware: Mapping[str, LabwareDefinition],
load_name: str,
namespace: Optional[str] = None,
version: Optional[int] = None,
Expand Down
Loading

0 comments on commit 41e0f6f

Please sign in to comment.