opentrons 8.4.0a4__py2.py3-none-any.whl → 8.4.0a5__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of opentrons might be problematic. Click here for more details.

@@ -12,6 +12,7 @@ from typing import (
12
12
  Tuple,
13
13
  NamedTuple,
14
14
  Generator,
15
+ Literal,
15
16
  )
16
17
  from opentrons.types import (
17
18
  Location,
@@ -747,6 +748,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
747
748
  force_direct: bool,
748
749
  minimum_z_height: Optional[float],
749
750
  speed: Optional[float],
751
+ check_for_movement_conflicts: bool = True,
750
752
  ) -> None:
751
753
  if well_core is not None:
752
754
  if isinstance(location, (TrashBin, WasteChute)):
@@ -765,6 +767,14 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
765
767
  assert isinstance(well_location, LiquidHandlingWellLocation)
766
768
  if well_location.volumeOffset and well_location.volumeOffset != 0:
767
769
  raise ValueError("volume offset not supported with move_to")
770
+ if check_for_movement_conflicts:
771
+ pipette_movement_conflict.check_safe_for_pipette_movement(
772
+ engine_state=self._engine_client.state,
773
+ pipette_id=self._pipette_id,
774
+ labware_id=labware_id,
775
+ well_name=well_name,
776
+ well_location=well_location,
777
+ )
768
778
  self._engine_client.execute_command(
769
779
  cmd.MoveToWellParams(
770
780
  pipetteId=self._pipette_id,
@@ -1399,7 +1409,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1399
1409
  volume: float,
1400
1410
  source: Tuple[Location, WellCore],
1401
1411
  dest: List[Tuple[Location, WellCore]],
1402
- new_tip: TransferTipPolicyV2,
1412
+ new_tip: Literal[TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE],
1403
1413
  tip_racks: List[Tuple[Location, LabwareCore]],
1404
1414
  starting_tip: Optional[WellCore],
1405
1415
  trash_location: Union[Location, TrashBin, WasteChute],
@@ -1415,8 +1425,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1415
1425
  dest: List of destination wells, with each well represented as a tuple of
1416
1426
  types.Location and WellCore.
1417
1427
  types.Location is only necessary for saving the last accessed location.
1418
- new_tip: Whether the transfer should use a new tip 'once', 'never', 'always',
1419
- or 'per source'.
1428
+ new_tip: Whether the transfer should use a new tip 'once' or 'never'.
1420
1429
  tiprack_uri: The URI of the tiprack that the transfer settings are for.
1421
1430
  tip_drop_location: Location where the tip will be dropped (if appropriate).
1422
1431
 
@@ -1432,6 +1441,8 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1432
1441
  raise RuntimeError(
1433
1442
  "No tipracks found for pipette in order to perform transfer"
1434
1443
  )
1444
+ assert new_tip in [TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE]
1445
+
1435
1446
  tiprack_uri_for_transfer_props = tip_racks[0][1].get_uri()
1436
1447
  working_volume = min(
1437
1448
  self.get_max_volume(),
@@ -1559,7 +1570,6 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1559
1570
  )
1560
1571
  return tip_well
1561
1572
 
1562
- tip_used = False
1563
1573
  if new_tip != TransferTipPolicyV2.NEVER:
1564
1574
  last_tip_picked_up_from = _pick_up_tip()
1565
1575
 
@@ -1604,16 +1614,6 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1604
1614
  )
1605
1615
  )
1606
1616
 
1607
- if new_tip == TransferTipPolicyV2.ALWAYS and tip_used:
1608
- _drop_tip()
1609
- last_tip_picked_up_from = _pick_up_tip()
1610
- tip_contents = [
1611
- tx_comps_executor.LiquidAndAirGapPair(
1612
- liquid=0,
1613
- air_gap=0,
1614
- )
1615
- ]
1616
-
1617
1617
  use_single_dispense = False
1618
1618
  if total_aspirate_volume == volume and len(vol_dest_combo) == 1:
1619
1619
  # We are only doing a single transfer. Either because this is the last
@@ -1696,7 +1696,6 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1696
1696
  disposal_volume=disposal_vol,
1697
1697
  )
1698
1698
  is_first_step = False
1699
- tip_used = True
1700
1699
 
1701
1700
  if new_tip != TransferTipPolicyV2.NEVER:
1702
1701
  _drop_tip()
@@ -1728,7 +1727,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1728
1727
  volume: float,
1729
1728
  source: List[Tuple[Location, WellCore]],
1730
1729
  dest: Tuple[Location, WellCore],
1731
- new_tip: TransferTipPolicyV2,
1730
+ new_tip: Literal[TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE],
1732
1731
  tip_racks: List[Tuple[Location, LabwareCore]],
1733
1732
  starting_tip: Optional[WellCore],
1734
1733
  trash_location: Union[Location, TrashBin, WasteChute],
@@ -1738,7 +1737,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1738
1737
  raise RuntimeError(
1739
1738
  "No tipracks found for pipette in order to perform transfer"
1740
1739
  )
1741
-
1740
+ assert new_tip in [TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE]
1742
1741
  tiprack_uri_for_transfer_props = tip_racks[0][1].get_uri()
1743
1742
  try:
1744
1743
  transfer_props = liquid_class.get_for(
@@ -1841,7 +1840,6 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1841
1840
  if new_tip == TransferTipPolicyV2.ONCE:
1842
1841
  last_tip_picked_up_from = _pick_up_tip()
1843
1842
 
1844
- prev_src: Optional[Tuple[Location, WellCore]] = None
1845
1843
  tip_contents = [
1846
1844
  tx_comps_executor.LiquidAndAirGapPair(
1847
1845
  liquid=0,
@@ -1849,6 +1847,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1849
1847
  )
1850
1848
  ]
1851
1849
  next_step_volume, next_source = next(source_per_volume_step)
1850
+ is_first_step = True
1852
1851
  is_last_step = False
1853
1852
  while not is_last_step:
1854
1853
  total_dispense_volume = 0.0
@@ -1863,21 +1862,17 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1863
1862
  is_last_step = True
1864
1863
  break
1865
1864
 
1866
- if new_tip == TransferTipPolicyV2.ALWAYS:
1867
- if prev_src is not None:
1868
- _drop_tip()
1869
- last_tip_picked_up_from = _pick_up_tip()
1870
- tip_contents = [
1871
- tx_comps_executor.LiquidAndAirGapPair(
1872
- liquid=0,
1873
- air_gap=0,
1874
- )
1875
- ]
1876
- # TODO (spp, 2025-03-24): add LPD feature when 'per source' tip policy is added for consolidate_liquid
1877
- with self.lpd_for_transfer(enable=False):
1878
- for step_num, (step_volume, step_source) in enumerate(
1879
- vol_aspirate_combo
1880
- ):
1865
+ if (
1866
+ self.get_liquid_presence_detection()
1867
+ and new_tip != TransferTipPolicyV2.NEVER
1868
+ and is_first_step
1869
+ ):
1870
+ enable_lpd = True
1871
+ else:
1872
+ enable_lpd = False
1873
+
1874
+ for step_num, (step_volume, step_source) in enumerate(vol_aspirate_combo):
1875
+ with self.lpd_for_transfer(enable=enable_lpd):
1881
1876
  tip_contents = self.aspirate_liquid_class(
1882
1877
  volume=step_volume,
1883
1878
  source=step_source,
@@ -1888,6 +1883,8 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1888
1883
  total_dispense_volume if step_num == 0 else None
1889
1884
  ),
1890
1885
  )
1886
+ is_first_step = False
1887
+ enable_lpd = False
1891
1888
  tip_contents = self.dispense_liquid_class(
1892
1889
  volume=total_dispense_volume,
1893
1890
  dest=dest,
@@ -1902,7 +1899,6 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1902
1899
  ),
1903
1900
  trash_location=trash_location,
1904
1901
  )
1905
- prev_src = next_source
1906
1902
  if new_tip != TransferTipPolicyV2.NEVER:
1907
1903
  _drop_tip()
1908
1904
 
@@ -99,6 +99,14 @@ class TipState:
99
99
  ), "Last air gap volume doe not match the volume being removed"
100
100
  self.last_liquid_and_air_gap_in_tip.air_gap = 0
101
101
 
102
+ def delete_last_air_gap_and_liquid(self) -> None:
103
+ air_gap_in_tip = self.last_liquid_and_air_gap_in_tip.air_gap
104
+ liquid_in_tip = self.last_liquid_and_air_gap_in_tip.liquid
105
+ if air_gap_in_tip:
106
+ self.delete_air_gap(air_gap_in_tip)
107
+ if liquid_in_tip:
108
+ self.delete_liquid(volume=liquid_in_tip)
109
+
102
110
 
103
111
  class TransferType(Enum):
104
112
  ONE_TO_ONE = "one_to_one"
@@ -525,6 +533,9 @@ class TransferComponentsExecutor:
525
533
  if isinstance(trash_location, Location)
526
534
  else None
527
535
  )
536
+ # A non-multi-dispense blowout will only have air and maybe droplets in the tip
537
+ # since we only blowout after dispensing the full tip contents.
538
+ # So delete the air gap from tip state
528
539
  last_air_gap = self._tip_state.last_liquid_and_air_gap_in_tip.air_gap
529
540
  self._tip_state.delete_air_gap(last_air_gap)
530
541
  self._tip_state.ready_to_aspirate = False
@@ -608,6 +619,10 @@ class TransferComponentsExecutor:
608
619
  well_core=None,
609
620
  in_place=True,
610
621
  )
622
+ # A blowout will remove all air gap and liquid (disposal volume) from the tip
623
+ # so delete them from tip state (although practically, there will not be
624
+ # any air gaps in the tip before blowing out in the destination well)
625
+ self._tip_state.delete_last_air_gap_and_liquid()
611
626
  self._tip_state.ready_to_aspirate = False
612
627
 
613
628
  # A retract will perform total of two air gaps if we need to blow out in source or trash:
@@ -695,8 +710,9 @@ class TransferComponentsExecutor:
695
710
  if isinstance(trash_location, Location)
696
711
  else None
697
712
  )
698
- last_air_gap = self._tip_state.last_liquid_and_air_gap_in_tip.air_gap
699
- self._tip_state.delete_air_gap(last_air_gap)
713
+ # A blowout will remove all air gap and liquid (disposal volume) from the tip
714
+ # so delete them from tip state
715
+ self._tip_state.delete_last_air_gap_and_liquid()
700
716
  self._tip_state.ready_to_aspirate = False
701
717
 
702
718
  # Do touch tip and air gap again after blowing out into source well or trash
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from abc import abstractmethod, ABC
6
- from typing import Any, Generic, Optional, TypeVar, Union, List, Tuple
6
+ from typing import Any, Generic, Optional, TypeVar, Union, List, Tuple, Literal
7
7
 
8
8
  from opentrons import types
9
9
  from opentrons.hardware_control.dev_types import PipetteDict
@@ -190,6 +190,7 @@ class AbstractInstrument(ABC, Generic[WellCoreType, LabwareCoreType]):
190
190
  force_direct: bool,
191
191
  minimum_z_height: Optional[float],
192
192
  speed: Optional[float],
193
+ check_for_movement_conflicts: bool,
193
194
  ) -> None:
194
195
  ...
195
196
 
@@ -381,7 +382,7 @@ class AbstractInstrument(ABC, Generic[WellCoreType, LabwareCoreType]):
381
382
  volume: float,
382
383
  source: Tuple[types.Location, WellCoreType],
383
384
  dest: List[Tuple[types.Location, WellCoreType]],
384
- new_tip: TransferTipPolicyV2,
385
+ new_tip: Literal[TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE],
385
386
  tip_racks: List[Tuple[types.Location, LabwareCoreType]],
386
387
  starting_tip: Optional[WellCoreType],
387
388
  trash_location: Union[types.Location, TrashBin, WasteChute],
@@ -400,7 +401,7 @@ class AbstractInstrument(ABC, Generic[WellCoreType, LabwareCoreType]):
400
401
  volume: float,
401
402
  source: List[Tuple[types.Location, WellCoreType]],
402
403
  dest: Tuple[types.Location, WellCoreType],
403
- new_tip: TransferTipPolicyV2,
404
+ new_tip: Literal[TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE],
404
405
  tip_racks: List[Tuple[types.Location, LabwareCoreType]],
405
406
  starting_tip: Optional[WellCoreType],
406
407
  trash_location: Union[types.Location, TrashBin, WasteChute],
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from typing import TYPE_CHECKING, Optional, Union, List, Tuple
4
+ from typing import TYPE_CHECKING, Optional, Union, List, Tuple, Literal
5
5
 
6
6
  from opentrons import types
7
7
  from opentrons.hardware_control import CriticalPoint
@@ -367,6 +367,7 @@ class LegacyInstrumentCore(AbstractInstrument[LegacyWellCore, LegacyLabwareCore]
367
367
  force_direct: bool = False,
368
368
  minimum_z_height: Optional[float] = None,
369
369
  speed: Optional[float] = None,
370
+ check_for_movement_conflicts: bool = False,
370
371
  ) -> None:
371
372
  """Move the instrument.
372
373
 
@@ -376,6 +377,7 @@ class LegacyInstrumentCore(AbstractInstrument[LegacyWellCore, LegacyLabwareCore]
376
377
  force_direct: Force a direct movement instead of an arc.
377
378
  minimum_z_height: Set a minimum travel height for a movement arc.
378
379
  speed: Override the travel speed in mm/s.
380
+ check_for_movement_conflicts: Not used in legacy implementation
379
381
 
380
382
  Raises:
381
383
  LabwareHeightError: An item on the deck is taller than
@@ -619,7 +621,7 @@ class LegacyInstrumentCore(AbstractInstrument[LegacyWellCore, LegacyLabwareCore]
619
621
  volume: float,
620
622
  source: Tuple[types.Location, LegacyWellCore],
621
623
  dest: List[Tuple[types.Location, LegacyWellCore]],
622
- new_tip: TransferTipPolicyV2,
624
+ new_tip: Literal[TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE],
623
625
  tip_racks: List[Tuple[types.Location, LegacyLabwareCore]],
624
626
  starting_tip: Optional[LegacyWellCore],
625
627
  trash_location: Union[types.Location, TrashBin, WasteChute],
@@ -634,7 +636,7 @@ class LegacyInstrumentCore(AbstractInstrument[LegacyWellCore, LegacyLabwareCore]
634
636
  volume: float,
635
637
  source: List[Tuple[types.Location, LegacyWellCore]],
636
638
  dest: Tuple[types.Location, LegacyWellCore],
637
- new_tip: TransferTipPolicyV2,
639
+ new_tip: Literal[TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE],
638
640
  tip_racks: List[Tuple[types.Location, LegacyLabwareCore]],
639
641
  starting_tip: Optional[LegacyWellCore],
640
642
  trash_location: Union[types.Location, TrashBin, WasteChute],
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from typing import TYPE_CHECKING, Optional, Union, List, Tuple
4
+ from typing import TYPE_CHECKING, Optional, Union, List, Tuple, Literal
5
5
 
6
6
  from opentrons import types
7
7
  from opentrons.hardware_control.dev_types import PipetteDict
@@ -325,6 +325,7 @@ class LegacyInstrumentCoreSimulator(
325
325
  force_direct: bool = False,
326
326
  minimum_z_height: Optional[float] = None,
327
327
  speed: Optional[float] = None,
328
+ check_for_movement_conflicts: bool = False, # Not used in this implementation
328
329
  ) -> None:
329
330
  """Simulation of only the motion planning portion of move_to."""
330
331
  if isinstance(location, (TrashBin, WasteChute)):
@@ -534,7 +535,7 @@ class LegacyInstrumentCoreSimulator(
534
535
  volume: float,
535
536
  source: Tuple[types.Location, LegacyWellCore],
536
537
  dest: List[Tuple[types.Location, LegacyWellCore]],
537
- new_tip: TransferTipPolicyV2,
538
+ new_tip: Literal[TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE],
538
539
  tip_racks: List[Tuple[types.Location, LegacyLabwareCore]],
539
540
  starting_tip: Optional[LegacyWellCore],
540
541
  trash_location: Union[types.Location, TrashBin, WasteChute],
@@ -549,7 +550,7 @@ class LegacyInstrumentCoreSimulator(
549
550
  volume: float,
550
551
  source: List[Tuple[types.Location, LegacyWellCore]],
551
552
  dest: Tuple[types.Location, LegacyWellCore],
552
- new_tip: TransferTipPolicyV2,
553
+ new_tip: Literal[TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE],
553
554
  tip_racks: List[Tuple[types.Location, LegacyLabwareCore]],
554
555
  starting_tip: Optional[LegacyWellCore],
555
556
  trash_location: Union[types.Location, TrashBin, WasteChute],
@@ -1529,7 +1529,7 @@ class InstrumentContext(publisher.CommandPublisher):
1529
1529
  """Move a particular type of liquid from one well or group of wells to another.
1530
1530
 
1531
1531
  :param liquid_class: The type of liquid to move. You must specify the liquid class,
1532
- even if you have used :py:meth:`.load_liquid` to indicate what liquid the
1532
+ even if you have used :py:meth:`.Labware.load_liquid` to indicate what liquid the
1533
1533
  source contains.
1534
1534
  :type liquid_class: :py:class:`.LiquidClass`
1535
1535
 
@@ -1552,6 +1552,8 @@ class InstrumentContext(publisher.CommandPublisher):
1552
1552
  tips. Depending on the liquid class, the pipette may also blow out liquid here.
1553
1553
  :param return_tip: Whether to drop used tips in their original locations
1554
1554
  in the tip rack, instead of the trash.
1555
+
1556
+ :meta private:
1555
1557
  """
1556
1558
  if volume == 0.0:
1557
1559
  _log.info(
@@ -1634,7 +1636,7 @@ class InstrumentContext(publisher.CommandPublisher):
1634
1636
  Distribute a particular type of liquid from one well to a group of wells.
1635
1637
 
1636
1638
  :param liquid_class: The type of liquid to move. You must specify the liquid class,
1637
- even if you have used :py:meth:`.load_liquid` to indicate what liquid the
1639
+ even if you have used :py:meth:`.Labware.load_liquid` to indicate what liquid the
1638
1640
  source contains.
1639
1641
  :type liquid_class: :py:class:`.LiquidClass`
1640
1642
 
@@ -1645,8 +1647,7 @@ class InstrumentContext(publisher.CommandPublisher):
1645
1647
  :param new_tip: When to pick up and drop tips during the command.
1646
1648
  Defaults to ``"once"``.
1647
1649
 
1648
- - ``"once"`` or ``"per source"``: Use one tip for the entire command.
1649
- - ``"always"``: Use a new tip for each set of aspirate and dispense steps.
1650
+ - ``"once"``: Use one tip for the entire command.
1650
1651
  - ``"never"``: Do not pick up or drop tips at all.
1651
1652
 
1652
1653
  See :ref:`param-tip-handling` for details.
@@ -1655,6 +1656,8 @@ class InstrumentContext(publisher.CommandPublisher):
1655
1656
  tips. Depending on the liquid class, the pipette may also blow out liquid here.
1656
1657
  :param return_tip: Whether to drop used tips in their original locations
1657
1658
  in the tip rack, instead of the trash.
1659
+
1660
+ :meta private:
1658
1661
  """
1659
1662
  if volume == 0.0:
1660
1663
  _log.info(
@@ -1681,9 +1684,14 @@ class InstrumentContext(publisher.CommandPublisher):
1681
1684
  f"Source should be a single well (or resolve to a single transfer for multi-channel) "
1682
1685
  f"but received {transfer_args.sources_list}."
1683
1686
  )
1684
- if transfer_args.tip_policy == TransferTipPolicyV2.PER_SOURCE:
1685
- raise RuntimeError(
1686
- 'Tip transfer policy "per source" incompatible with distribute.'
1687
+ if transfer_args.tip_policy not in [
1688
+ TransferTipPolicyV2.ONCE,
1689
+ TransferTipPolicyV2.NEVER,
1690
+ ]:
1691
+ raise ValueError(
1692
+ f"Incompatible `new_tip` value of {new_tip}."
1693
+ f" `distribute_with_liquid_class()` only supports `new_tip` values of"
1694
+ f" 'once' and 'never'."
1687
1695
  )
1688
1696
 
1689
1697
  verified_source = transfer_args.sources_list[0]
@@ -1708,7 +1716,7 @@ class InstrumentContext(publisher.CommandPublisher):
1708
1716
  (types.Location(types.Point(), labware=well), well._core)
1709
1717
  for well in transfer_args.destinations_list
1710
1718
  ],
1711
- new_tip=transfer_args.tip_policy,
1719
+ new_tip=transfer_args.tip_policy, # type: ignore[arg-type]
1712
1720
  tip_racks=[
1713
1721
  (types.Location(types.Point(), labware=rack), rack._core)
1714
1722
  for rack in transfer_args.tip_racks
@@ -1741,7 +1749,7 @@ class InstrumentContext(publisher.CommandPublisher):
1741
1749
  Consolidate a particular type of liquid from a group of wells to one well.
1742
1750
 
1743
1751
  :param liquid_class: The type of liquid to move. You must specify the liquid class,
1744
- even if you have used :py:meth:`.load_liquid` to indicate what liquid the
1752
+ even if you have used :py:meth:`.Labware.load_liquid` to indicate what liquid the
1745
1753
  source contains.
1746
1754
  :type liquid_class: :py:class:`.LiquidClass`
1747
1755
 
@@ -1753,8 +1761,6 @@ class InstrumentContext(publisher.CommandPublisher):
1753
1761
  Defaults to ``"once"``.
1754
1762
 
1755
1763
  - ``"once"``: Use one tip for the entire command.
1756
- - ``"always"``: Use a new tip for each set of aspirate and dispense steps.
1757
- - ``"per source"``: Not available when consolidating.
1758
1764
  - ``"never"``: Do not pick up or drop tips at all.
1759
1765
 
1760
1766
  See :ref:`param-tip-handling` for details.
@@ -1763,6 +1769,8 @@ class InstrumentContext(publisher.CommandPublisher):
1763
1769
  tips. Depending on the liquid class, the pipette may also blow out liquid here.
1764
1770
  :param return_tip: Whether to drop used tips in their original locations
1765
1771
  in the tip rack, instead of the trash.
1772
+
1773
+ :meta private:
1766
1774
  """
1767
1775
  if volume == 0.0:
1768
1776
  _log.info(
@@ -1789,9 +1797,14 @@ class InstrumentContext(publisher.CommandPublisher):
1789
1797
  f"Destination should be a single well (or resolve to a single transfer for multi-channel) "
1790
1798
  f"but received {transfer_args.destinations_list}."
1791
1799
  )
1792
- if transfer_args.tip_policy == TransferTipPolicyV2.PER_SOURCE:
1793
- raise RuntimeError(
1794
- 'Tip transfer policy "per source" incompatible with consolidate.'
1800
+ if transfer_args.tip_policy not in [
1801
+ TransferTipPolicyV2.ONCE,
1802
+ TransferTipPolicyV2.NEVER,
1803
+ ]:
1804
+ raise ValueError(
1805
+ f"Incompatible `new_tip` value of {new_tip}."
1806
+ f" `consolidate_with_liquid_class()` only supports `new_tip` values of"
1807
+ f" 'once' and 'never'."
1795
1808
  )
1796
1809
 
1797
1810
  verified_dest = transfer_args.destinations_list[0]
@@ -1816,7 +1829,7 @@ class InstrumentContext(publisher.CommandPublisher):
1816
1829
  types.Location(types.Point(), labware=verified_dest),
1817
1830
  verified_dest._core,
1818
1831
  ),
1819
- new_tip=transfer_args.tip_policy,
1832
+ new_tip=transfer_args.tip_policy, # type: ignore[arg-type]
1820
1833
  tip_racks=[
1821
1834
  (types.Location(types.Point(), labware=rack), rack._core)
1822
1835
  for rack in transfer_args.tip_racks
@@ -1907,6 +1920,7 @@ class InstrumentContext(publisher.CommandPublisher):
1907
1920
  force_direct=force_direct,
1908
1921
  minimum_z_height=minimum_z_height,
1909
1922
  speed=speed,
1923
+ check_for_movement_conflicts=False,
1910
1924
  )
1911
1925
  else:
1912
1926
  if publish:
@@ -1925,6 +1939,7 @@ class InstrumentContext(publisher.CommandPublisher):
1925
1939
  force_direct=force_direct,
1926
1940
  minimum_z_height=minimum_z_height,
1927
1941
  speed=speed,
1942
+ check_for_movement_conflicts=False,
1928
1943
  )
1929
1944
 
1930
1945
  return self
@@ -263,15 +263,15 @@ class Well:
263
263
 
264
264
  @requires_version(2, 21)
265
265
  def meniscus(
266
- self, target: Literal["start", "end", "dynamic"], z: float = 0.0
266
+ self, z: float = 0.0, target: Literal["start", "end", "dynamic"] = "end"
267
267
  ) -> Location:
268
268
  """
269
269
  :param z: An offset on the z-axis, in mm. Positive offsets are higher and
270
270
  negative offsets are lower.
271
- :param target: The relative position inside the well to target when performing a liquid handling operation.
272
- :return: A :py:class:`~opentrons.types.Location` that indicates location is meniscus and that holds the ``z`` offset in its point.z field.
271
+ :param target: The relative position of the liquid meniscus inside the well to target when performing a liquid handling operation.
272
+
273
+ :return: A :py:class:`~opentrons.types.Location` corresponding to the liquid meniscus, plus a target position and ``z`` offset as specified.
273
274
 
274
- :meta private:
275
275
  """
276
276
  return Location(
277
277
  point=Point(x=0, y=0, z=z),
@@ -326,9 +326,9 @@ class Well:
326
326
  :param Liquid liquid: The liquid to load into the well.
327
327
  :param float volume: The volume of liquid to load, in µL.
328
328
 
329
- .. TODO: flag as deprecated in 2.22 docs
330
- In API version 2.22 and later, use :py:meth:`~Labware.load_liquid`, :py:meth:`~Labware.load_liquid_by_well`,
331
- or :py:meth:`~Labware.load_empty` to load liquid into a well.
329
+ .. deprecated:: 2.22
330
+ Use :py:meth:`.Labware.load_liquid`, :py:meth:`.Labware.load_liquid_by_well`, or :py:meth:`.Labware.load_empty` instead.
331
+
332
332
  """
333
333
  self._core.load_liquid(
334
334
  liquid=liquid,
@@ -1270,22 +1270,15 @@ class Labware:
1270
1270
  ) -> None:
1271
1271
  """Mark several wells as containing the same amount of liquid.
1272
1272
 
1273
- This method should be called at the beginning of a protocol, soon after loading the labware and before
1274
- liquid handling operations begin. It is a base of information for liquid tracking functionality. If a well in a labware
1275
- has not been named in a call to :py:meth:`~Labware.load_empty`, :py:meth:`~Labware.load_liquid`, or
1276
- :py:meth:`~Labware.load_liquid_by_well`, the volume it contains is unknown and the well's liquid will not be tracked.
1277
-
1278
- For example, to load 10µL of a liquid named ``water`` (defined with :py:meth:`~ProtocolContext.define_liquid`)
1279
- into all the wells of a labware, you could call ``labware.load_liquid(labware.wells(), 10, water)``.
1280
-
1281
- If you want to load different volumes of liquid into different wells, use :py:meth:`~Labware.load_liquid_by_well`.
1282
-
1283
- If you want to mark the well as containing no liquid, use :py:meth:`~Labware.load_empty`.
1273
+ This method should be called at the beginning of a protocol, soon after loading labware and before
1274
+ liquid handling operations begin. Loading liquids is required for liquid tracking functionality. If a well
1275
+ hasn't been assigned a starting volume with :py:meth:`~Labware.load_empty`, :py:meth:`~Labware.load_liquid`, or
1276
+ :py:meth:`~Labware.load_liquid_by_well`, the volume it contains is unknown and the well's liquid will not be tracked throughout the protocol.
1284
1277
 
1285
1278
  :param wells: The wells to load the liquid into.
1286
- :type wells: List of well names or list of Well objects, for instance from :py:meth:`~Labware.wells`.
1279
+ :type wells: List of string well names or list of :py:class:`.Well` objects (e.g., from :py:meth:`~Labware.wells`).
1287
1280
 
1288
- :param volume: The volume of liquid to load into each well, in 10µL.
1281
+ :param volume: The volume of liquid to load into each well.
1289
1282
  :type volume: float
1290
1283
 
1291
1284
  :param liquid: The liquid to load into each well, previously defined by :py:meth:`~ProtocolContext.define_liquid`
@@ -1321,18 +1314,9 @@ class Labware:
1321
1314
  ) -> None:
1322
1315
  """Mark several wells as containing unique volumes of liquid.
1323
1316
 
1324
- This method should be called at the beginning of a protocol, soon after loading the labware and before
1325
- liquid handling operations begin. It is a base of information for liquid tracking functionality. If a well in a labware
1326
- has not been named in a call to :py:meth:`~Labware.load_empty`, :py:meth:`~Labware.load_liquid`, or
1327
- :py:meth:`~Labware.load_liquid_by_well`, the volume it contains is unknown and the well's liquid will not be tracked.
1328
-
1329
- For example, to load a decreasing amount of of a liquid named ``water`` (defined with :py:meth:`~ProtocolContext.define_liquid`)
1330
- into each successive well of a row, you could call
1331
- ``labware.load_liquid_by_well({'A1': 1000, 'A2': 950, 'A3': 900, ..., 'A12': 600}, water)``
1332
-
1333
- If you want to load the same volume of a liquid into multiple wells, it is often easier to use :py:meth:`~Labware.load_liquid`.
1334
-
1335
- If you want to mark the well as containing no liquid, use :py:meth:`~Labware.load_empty`.
1317
+ This method should be called at the beginning of a protocol, soon after loading labware and before
1318
+ liquid handling begins. Loading liquids is required for liquid tracking functionality. If a well hasn't been assigned a starting volume with :py:meth:`~Labware.load_empty`, :py:meth:`~Labware.load_liquid`, or
1319
+ :py:meth:`~Labware.load_liquid_by_well`, the volume it contains is unknown and the well's liquid will not be tracked throughout the protocol.
1336
1320
 
1337
1321
  :param volumes: A dictionary of well names (or :py:class:`Well` objects, for instance from ``labware['A1']``)
1338
1322
  :type wells: Dict[Union[str, Well], float]
@@ -1368,12 +1352,9 @@ class Labware:
1368
1352
  def load_empty(self, wells: Sequence[Union[Well, str]]) -> None:
1369
1353
  """Mark several wells as empty.
1370
1354
 
1371
- This method should be called at the beginning of a protocol, soon after loading the labware and before liquid handling
1372
- operations begin. It is a base of information for liquid tracking functionality. If a well in a labware has not been named
1373
- in a call to :py:meth:`Labware.load_empty`, :py:meth:`Labware.load_liquid`, or :py:meth:`Labware.load_liquid_by_well`, the
1374
- volume it contains is unknown and the well's liquid will not be tracked.
1375
-
1376
- For instance, to mark all wells in the labware as empty, you can call ``labware.load_empty(labware.wells())``.
1355
+ This method should be called at the beginning of a protocol, after loading the labware and before liquid handling
1356
+ begins. Loading liquids is required for liquid tracking functionality. If a well in a labware hasn't been assigned a starting volume with :py:meth:`Labware.load_empty`, :py:meth:`Labware.load_liquid`, or :py:meth:`Labware.load_liquid_by_well`, the
1357
+ volume it contains is unknown and the well's liquid will not be tracked throughout the protocol.
1377
1358
 
1378
1359
  :param wells: The list of wells to mark empty. To mark all wells as empty, pass ``labware.wells()``. You can also specify
1379
1360
  wells by their names (for instance, ``labware.load_empty(['A1', 'A2'])``).
@@ -29,7 +29,7 @@ from ..state.update_types import StateUpdate
29
29
  from ..errors.exceptions import PipetteNotReadyToAspirateError
30
30
  from opentrons.hardware_control import HardwareControlAPI
31
31
  from ..state.update_types import CLEAR
32
- from ..types import CurrentWell, DeckPoint
32
+ from ..types import DeckPoint
33
33
 
34
34
  if TYPE_CHECKING:
35
35
  from ..execution import PipettingHandler, GantryMover, MovementHandler
@@ -104,11 +104,8 @@ class AspirateWhileTrackingImplementation(
104
104
  " The first aspirate following a blow-out must be from a specific well"
105
105
  " so the plunger can be reset in a known safe position."
106
106
  )
107
-
108
- current_position = await self._gantry_mover.get_position(params.pipetteId)
109
- current_location = self._state_view.pipettes.get_current_location()
110
-
111
107
  state_update = StateUpdate()
108
+
112
109
  move_result = await move_to_well(
113
110
  movement=self._movement,
114
111
  model_utils=self._model_utils,
@@ -132,9 +129,9 @@ class AspirateWhileTrackingImplementation(
132
129
  flow_rate=params.flowRate,
133
130
  location_if_error={
134
131
  "retryLocation": (
135
- current_position.x,
136
- current_position.y,
137
- current_position.z,
132
+ move_result.public.position.x,
133
+ move_result.public.position.y,
134
+ move_result.public.position.z,
138
135
  )
139
136
  },
140
137
  command_note_adder=self._command_note_adder,
@@ -150,58 +147,40 @@ class AspirateWhileTrackingImplementation(
150
147
  z=position_after_aspirate.z,
151
148
  )
152
149
  if isinstance(aspirate_result, DefinedErrorData):
153
- if (
154
- isinstance(current_location, CurrentWell)
155
- and current_location.pipette_id == params.pipetteId
156
- ):
157
- return DefinedErrorData(
158
- public=aspirate_result.public,
159
- state_update=aspirate_result.state_update.set_liquid_operated(
160
- labware_id=current_location.labware_id,
161
- well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
162
- current_location.labware_id,
163
- current_location.well_name,
164
- params.pipetteId,
165
- ),
166
- volume_added=CLEAR,
167
- ),
168
- state_update_if_false_positive=aspirate_result.state_update_if_false_positive,
169
- )
170
- else:
171
- return aspirate_result
172
- else:
173
- if (
174
- isinstance(current_location, CurrentWell)
175
- and current_location.pipette_id == params.pipetteId
176
- ):
177
- return SuccessData(
178
- public=AspirateWhileTrackingResult(
179
- volume=aspirate_result.public.volume,
180
- position=result_deck_point,
181
- ),
182
- state_update=aspirate_result.state_update.set_liquid_operated(
183
- labware_id=current_location.labware_id,
184
- well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
185
- current_location.labware_id,
186
- current_location.well_name,
187
- params.pipetteId,
188
- ),
189
- volume_added=-aspirate_result.public.volume
190
- * self._state_view.geometry.get_nozzles_per_well(
191
- current_location.labware_id,
192
- current_location.well_name,
193
- params.pipetteId,
194
- ),
195
- ),
196
- )
197
- else:
198
- return SuccessData(
199
- public=AspirateWhileTrackingResult(
200
- volume=aspirate_result.public.volume,
201
- position=result_deck_point,
150
+ return DefinedErrorData(
151
+ public=aspirate_result.public,
152
+ state_update=aspirate_result.state_update.set_liquid_operated(
153
+ labware_id=params.labwareId,
154
+ well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
155
+ params.labwareId,
156
+ params.wellName,
157
+ params.pipetteId,
202
158
  ),
203
- state_update=aspirate_result.state_update,
204
- )
159
+ volume_added=CLEAR,
160
+ ),
161
+ state_update_if_false_positive=aspirate_result.state_update_if_false_positive,
162
+ )
163
+
164
+ return SuccessData(
165
+ public=AspirateWhileTrackingResult(
166
+ volume=aspirate_result.public.volume,
167
+ position=result_deck_point,
168
+ ),
169
+ state_update=aspirate_result.state_update.set_liquid_operated(
170
+ labware_id=params.labwareId,
171
+ well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
172
+ params.labwareId,
173
+ params.wellName,
174
+ params.pipetteId,
175
+ ),
176
+ volume_added=-aspirate_result.public.volume
177
+ * self._state_view.geometry.get_nozzles_per_well(
178
+ params.labwareId,
179
+ params.wellName,
180
+ params.pipetteId,
181
+ ),
182
+ ),
183
+ )
205
184
 
206
185
 
207
186
  class AspirateWhileTracking(
@@ -9,7 +9,7 @@ from pydantic import Field
9
9
  from pydantic.json_schema import SkipJsonSchema
10
10
 
11
11
  from ..state.update_types import CLEAR, StateUpdate
12
- from ..types import CurrentWell, DeckPoint
12
+ from ..types import DeckPoint
13
13
  from .pipetting_common import (
14
14
  PipetteIdMixin,
15
15
  DispenseVolumeMixin,
@@ -99,9 +99,6 @@ class DispenseWhileTrackingImplementation(
99
99
 
100
100
  # TODO(pbm, 10-15-24): call self._state_view.geometry.validate_dispense_volume_into_well()
101
101
 
102
- current_location = self._state_view.pipettes.get_current_location()
103
- current_position = await self._gantry_mover.get_position(params.pipetteId)
104
-
105
102
  state_update = StateUpdate()
106
103
  move_result = await move_to_well(
107
104
  movement=self._movement,
@@ -110,7 +107,6 @@ class DispenseWhileTrackingImplementation(
110
107
  labware_id=params.labwareId,
111
108
  well_name=params.wellName,
112
109
  well_location=params.wellLocation,
113
- operation_volume=-params.volume,
114
110
  )
115
111
  state_update.append(move_result.state_update)
116
112
  if isinstance(move_result, DefinedErrorData):
@@ -127,9 +123,9 @@ class DispenseWhileTrackingImplementation(
127
123
  push_out=params.pushOut,
128
124
  location_if_error={
129
125
  "retryLocation": (
130
- current_position.x,
131
- current_position.y,
132
- current_position.z,
126
+ move_result.public.position.x,
127
+ move_result.public.position.y,
128
+ move_result.public.position.z,
133
129
  )
134
130
  },
135
131
  pipetting=self._pipetting,
@@ -145,67 +141,40 @@ class DispenseWhileTrackingImplementation(
145
141
  )
146
142
 
147
143
  if isinstance(dispense_result, DefinedErrorData):
148
- if (
149
- isinstance(current_location, CurrentWell)
150
- and current_location.pipette_id == params.pipetteId
151
- ):
152
- return DefinedErrorData(
153
- public=dispense_result.public,
154
- state_update=dispense_result.state_update.set_liquid_operated(
155
- labware_id=current_location.labware_id,
156
- well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
157
- current_location.labware_id,
158
- current_location.well_name,
159
- params.pipetteId,
160
- ),
161
- volume_added=CLEAR,
162
- ),
163
- state_update_if_false_positive=dispense_result.state_update_if_false_positive,
164
- )
165
- else:
166
- return dispense_result
167
- else:
168
- if (
169
- isinstance(current_location, CurrentWell)
170
- and current_location.pipette_id == params.pipetteId
171
- ):
172
- volume_added = (
173
- self._state_view.pipettes.get_liquid_dispensed_by_ejecting_volume(
174
- pipette_id=params.pipetteId,
175
- volume=dispense_result.public.volume,
176
- )
177
- )
178
- if volume_added is not None:
179
- volume_added *= self._state_view.geometry.get_nozzles_per_well(
180
- current_location.labware_id,
181
- current_location.well_name,
144
+ return DefinedErrorData(
145
+ public=dispense_result.public,
146
+ state_update=dispense_result.state_update.set_liquid_operated(
147
+ labware_id=params.labwareId,
148
+ well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
149
+ params.labwareId,
150
+ params.wellName,
182
151
  params.pipetteId,
183
- )
184
- return SuccessData(
185
- public=DispenseWhileTrackingResult(
186
- volume=dispense_result.public.volume,
187
- position=result_deck_point,
188
- ),
189
- state_update=dispense_result.state_update.set_liquid_operated(
190
- labware_id=current_location.labware_id,
191
- well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
192
- current_location.labware_id,
193
- current_location.well_name,
194
- params.pipetteId,
195
- ),
196
- volume_added=volume_added
197
- if volume_added is not None
198
- else CLEAR,
199
152
  ),
200
- )
201
- else:
202
- return SuccessData(
203
- public=DispenseWhileTrackingResult(
204
- volume=dispense_result.public.volume,
205
- position=result_deck_point,
206
- ),
207
- state_update=dispense_result.state_update,
208
- )
153
+ volume_added=CLEAR,
154
+ ),
155
+ state_update_if_false_positive=dispense_result.state_update_if_false_positive,
156
+ )
157
+
158
+ return SuccessData(
159
+ public=DispenseWhileTrackingResult(
160
+ volume=dispense_result.public.volume,
161
+ position=result_deck_point,
162
+ ),
163
+ state_update=dispense_result.state_update.set_liquid_operated(
164
+ labware_id=params.labwareId,
165
+ well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
166
+ params.labwareId,
167
+ params.wellName,
168
+ params.pipetteId,
169
+ ),
170
+ volume_added=dispense_result.public.volume
171
+ * self._state_view.geometry.get_nozzles_per_well(
172
+ params.labwareId,
173
+ params.wellName,
174
+ params.pipetteId,
175
+ ),
176
+ ),
177
+ )
209
178
 
210
179
 
211
180
  class DispenseWhileTracking(
@@ -246,6 +246,7 @@ class HardwarePipettingHandler(PipettingHandler):
246
246
  flow_rate=flow_rate,
247
247
  volume=adjusted_volume,
248
248
  push_out=push_out,
249
+ is_full_dispense=is_full_dispense,
249
250
  )
250
251
  return adjusted_volume
251
252
 
@@ -1,7 +1,8 @@
1
1
  import logging
2
2
  from logging.config import dictConfig
3
+ from logging.handlers import QueueListener, RotatingFileHandler
3
4
  import sys
4
- from typing import Any, Dict
5
+ from queue import Queue
5
6
 
6
7
  from opentrons.config import CONFIG, ARCHITECTURE, SystemArchitecture
7
8
 
@@ -12,11 +13,33 @@ else:
12
13
  SENSOR_LOG_NAME = "unused"
13
14
 
14
15
 
15
- def _host_config(level_value: int) -> Dict[str, Any]:
16
+ # We want this big enough to smooth over any temporary stalls in journald's ability
17
+ # to consume our records--but bounded, so if we consistently outpace journald for
18
+ # some reason, we don't leak memory or get latency from buffer bloat.
19
+ # 50000 is basically an arbitrary guess.
20
+ _LOG_QUEUE_SIZE = 50000
21
+
22
+
23
+ log_queue = Queue[logging.LogRecord](maxsize=_LOG_QUEUE_SIZE)
24
+ """A buffer through which log records will pass.
25
+
26
+ This is intended to work around problems when our logs are going to journald:
27
+ we think journald can block for a while when it flushes records to the filesystem,
28
+ and the backpressure from that will cause calls like `log.debug()` to block and
29
+ interfere with timing-sensitive hardware control.
30
+ https://github.com/Opentrons/opentrons/issues/18034
31
+
32
+ `log_init()` will configure all the logs that this package knows about to pass through
33
+ this queue. This queue is exposed so consumers of this package (i.e. robot-server)
34
+ can do the same thing with their own logs, which is important to preserve ordering.
35
+ """
36
+
37
+
38
+ def _config_for_host(level_value: int) -> None:
16
39
  serial_log_filename = CONFIG["serial_log_file"]
17
40
  api_log_filename = CONFIG["api_log_file"]
18
41
  sensor_log_filename = CONFIG["sensor_log_file"]
19
- return {
42
+ config = {
20
43
  "version": 1,
21
44
  "disable_existing_loggers": False,
22
45
  "formatters": {
@@ -90,13 +113,20 @@ def _host_config(level_value: int) -> Dict[str, Any]:
90
113
  },
91
114
  }
92
115
 
116
+ dictConfig(config)
93
117
 
94
- def _buildroot_config(level_value: int) -> Dict[str, Any]:
118
+
119
+ def _config_for_robot(level_value: int) -> None:
95
120
  # Import systemd.journald here since it is generally unavailble on non
96
121
  # linux systems and we probably don't want to use it on linux desktops
97
122
  # either
123
+ from systemd.journal import JournalHandler # type: ignore
124
+
98
125
  sensor_log_filename = CONFIG["sensor_log_file"]
99
- return {
126
+
127
+ sensor_log_queue = Queue[logging.LogRecord](maxsize=_LOG_QUEUE_SIZE)
128
+
129
+ config = {
100
130
  "version": 1,
101
131
  "disable_existing_loggers": False,
102
132
  "formatters": {
@@ -104,36 +134,38 @@ def _buildroot_config(level_value: int) -> Dict[str, Any]:
104
134
  },
105
135
  "handlers": {
106
136
  "api": {
107
- "class": "systemd.journal.JournalHandler",
137
+ "class": "opentrons.util.logging_queue_handler.CustomQueueHandler",
108
138
  "level": logging.DEBUG,
109
139
  "formatter": "message_only",
110
- "SYSLOG_IDENTIFIER": "opentrons-api",
140
+ "extra": {"SYSLOG_IDENTIFIER": "opentrons-api"},
141
+ "queue": log_queue,
111
142
  },
112
143
  "serial": {
113
- "class": "systemd.journal.JournalHandler",
144
+ "class": "opentrons.util.logging_queue_handler.CustomQueueHandler",
114
145
  "level": logging.DEBUG,
115
146
  "formatter": "message_only",
116
- "SYSLOG_IDENTIFIER": "opentrons-api-serial",
147
+ "extra": {"SYSLOG_IDENTIFIER": "opentrons-api-serial"},
148
+ "queue": log_queue,
117
149
  },
118
150
  "can_serial": {
119
- "class": "systemd.journal.JournalHandler",
151
+ "class": "opentrons.util.logging_queue_handler.CustomQueueHandler",
120
152
  "level": logging.DEBUG,
121
153
  "formatter": "message_only",
122
- "SYSLOG_IDENTIFIER": "opentrons-api-serial-can",
154
+ "extra": {"SYSLOG_IDENTIFIER": "opentrons-api-serial-can"},
155
+ "queue": log_queue,
123
156
  },
124
157
  "usbbin_serial": {
125
- "class": "systemd.journal.JournalHandler",
158
+ "class": "opentrons.util.logging_queue_handler.CustomQueueHandler",
126
159
  "level": logging.DEBUG,
127
160
  "formatter": "message_only",
128
- "SYSLOG_IDENTIFIER": "opentrons-api-serial-usbbin",
161
+ "extra": {"SYSLOG_IDENTIFIER": "opentrons-api-serial-usbbin"},
162
+ "queue": log_queue,
129
163
  },
130
164
  "sensor": {
131
- "class": "logging.handlers.RotatingFileHandler",
132
- "formatter": "message_only",
133
- "filename": sensor_log_filename,
134
- "maxBytes": 1000000,
165
+ "class": "opentrons.util.logging_queue_handler.CustomQueueHandler",
135
166
  "level": logging.DEBUG,
136
- "backupCount": 3,
167
+ "formatter": "message_only",
168
+ "queue": sensor_log_queue,
137
169
  },
138
170
  },
139
171
  "loggers": {
@@ -169,12 +201,47 @@ def _buildroot_config(level_value: int) -> Dict[str, Any]:
169
201
  },
170
202
  }
171
203
 
204
+ # Start draining from the queue and sending messages to journald.
205
+ # Then, stash the queue listener in a global variable so it doesn't get garbage-collected.
206
+ # I don't know if we actually need to do this, but let's not find out the hard way.
207
+ global _queue_listener
208
+ if _queue_listener is not None:
209
+ # In case this log init function was called multiple times for some reason.
210
+ _queue_listener.stop()
211
+ _queue_listener = QueueListener(log_queue, JournalHandler())
212
+ _queue_listener.start()
213
+
214
+ # Sensor logs are a special one-off thing that go to their own file instead of journald.
215
+ # We apply the same QueueListener performance workaround for basically the same reasons.
216
+ sensor_rotating_file_handler = RotatingFileHandler(
217
+ filename=sensor_log_filename, maxBytes=1000000, backupCount=3
218
+ )
219
+ sensor_rotating_file_handler.setLevel(logging.DEBUG)
220
+ sensor_rotating_file_handler.setFormatter(logging.Formatter(fmt="%(message)s"))
221
+ global _sensor_queue_listener
222
+ if _sensor_queue_listener is not None:
223
+ _sensor_queue_listener.stop()
224
+ _sensor_queue_listener = QueueListener(
225
+ sensor_log_queue, sensor_rotating_file_handler
226
+ )
227
+ _sensor_queue_listener.start()
228
+
229
+ dictConfig(config)
172
230
 
173
- def _config(arch: SystemArchitecture, level_value: int) -> Dict[str, Any]:
174
- return {
175
- SystemArchitecture.YOCTO: _buildroot_config,
176
- SystemArchitecture.BUILDROOT: _buildroot_config,
177
- SystemArchitecture.HOST: _host_config,
231
+ # TODO(2025-04-15): We need some kind of log_deinit() function to call
232
+ # queue_listener.stop() before the process ends. Not doing that means we're
233
+ # dropping some records when the process shuts down.
234
+
235
+
236
+ _queue_listener: QueueListener | None = None
237
+ _sensor_queue_listener: QueueListener | None = None
238
+
239
+
240
+ def _config(arch: SystemArchitecture, level_value: int) -> None:
241
+ {
242
+ SystemArchitecture.YOCTO: _config_for_robot,
243
+ SystemArchitecture.BUILDROOT: _config_for_robot,
244
+ SystemArchitecture.HOST: _config_for_host,
178
245
  }[arch](level_value)
179
246
 
180
247
 
@@ -191,6 +258,8 @@ def log_init(level_name: str) -> None:
191
258
  f"Defaulting to {fallback_log_level}\n"
192
259
  )
193
260
  ot_log_level = fallback_log_level
261
+
262
+ # todo(mm, 2025-04-14): Use logging.getLevelNamesMapping() when we have Python >=3.11.
194
263
  level_value = logging._nameToLevel[ot_log_level]
195
- logging_config = _config(ARCHITECTURE, level_value)
196
- dictConfig(logging_config)
264
+
265
+ _config(ARCHITECTURE, level_value)
@@ -0,0 +1,61 @@
1
+ # noqa: D100
2
+
3
+
4
+ import logging.handlers
5
+ import logging
6
+ from queue import Queue
7
+ from typing import cast
8
+ from typing_extensions import override
9
+
10
+
11
+ class CustomQueueHandler(logging.handlers.QueueHandler):
12
+ """A logging.QueueHandler with some customizations.
13
+
14
+ - Allow adding `extra` data to handled log records.
15
+
16
+ - Simplify and optimize for single-process use.
17
+
18
+ - If a new message comes in but the queue is full, block until it has room.
19
+ (The default QueueHandler drops records in a way we probably wouldn't notice.)
20
+ """
21
+
22
+ def __init__(
23
+ self, *, queue: Queue[logging.LogRecord], extra: dict[str, object] | None = None
24
+ ) -> None:
25
+ """Construct the handler.
26
+
27
+ Args:
28
+ queue: When this handler receives a log record, it will insert the message
29
+ into this queue.
30
+ extra: Extra data to attach to each log record, to be interpreted by
31
+ whatever handler is on the consuming side of the queue. e.g. if that's
32
+ `systemd.journal.JournalHandler`, you could add a "SYSLOG_IDENTIFIER"
33
+ key here. This corresponds to the `extra` arg of `Logger.debug()`.
34
+ """
35
+ super().__init__(queue=queue)
36
+
37
+ # Double underscore because we're subclassing external code so we should try to
38
+ # avoid collisions with its attributes.
39
+ self.__extra = extra
40
+
41
+ @override
42
+ def prepare(self, record: logging.LogRecord) -> logging.LogRecord:
43
+ """Called internally by the superclass before enqueueing a record."""
44
+ if self.__extra is not None:
45
+ # This looks questionable, but updating __dict__ is the documented behavior
46
+ # of `Logger.debug(msg, extra=...)`.
47
+ record.__dict__.update(self.__extra)
48
+
49
+ # We intentionally do *not* call `super().prepare(record)`. It's documented to
50
+ # muck with the data in the LogRecord, apparently as part of supporting
51
+ # inter-process use. Since we don't need that, we can preserve the original
52
+ # data and also save some compute time.
53
+ return record
54
+
55
+ @override
56
+ def enqueue(self, record: logging.LogRecord) -> None:
57
+ """Called internally by the superclass to enqueue a record."""
58
+ # This cast is safe because we constrain the type of `self.queue`
59
+ # in our `__init__()` and nobody should mutate it after-the-fact, in practice.
60
+ queue = cast(Queue[logging.LogRecord], self.queue)
61
+ queue.put(record)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: opentrons
3
- Version: 8.4.0a4
3
+ Version: 8.4.0a5
4
4
  Summary: The Opentrons API is a simple framework designed to make writing automated biology lab protocols easy.
5
5
  Author: Opentrons
6
6
  Author-email: engineering@opentrons.com
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.10
21
21
  Classifier: Topic :: Scientific/Engineering
22
22
  Requires-Python: >=3.10
23
23
  License-File: ../LICENSE
24
- Requires-Dist: opentrons-shared-data (==8.4.0a4)
24
+ Requires-Dist: opentrons-shared-data (==8.4.0a5)
25
25
  Requires-Dist: aionotify (==0.3.1)
26
26
  Requires-Dist: anyio (<4.0.0,>=3.6.1)
27
27
  Requires-Dist: jsonschema (<4.18.0,>=3.0.1)
@@ -35,9 +35,9 @@ Requires-Dist: pyusb (==1.2.1)
35
35
  Requires-Dist: packaging (>=21.0)
36
36
  Requires-Dist: importlib-metadata (>=1.0) ; python_version < "3.8"
37
37
  Provides-Extra: flex-hardware
38
- Requires-Dist: opentrons-hardware[flex] (==8.4.0a4) ; extra == 'flex-hardware'
38
+ Requires-Dist: opentrons-hardware[flex] (==8.4.0a5) ; extra == 'flex-hardware'
39
39
  Provides-Extra: ot2-hardware
40
- Requires-Dist: opentrons-hardware (==8.4.0a4) ; extra == 'ot2-hardware'
40
+ Requires-Dist: opentrons-hardware (==8.4.0a5) ; extra == 'ot2-hardware'
41
41
 
42
42
  .. _Full API Documentation: http://docs.opentrons.com
43
43
 
@@ -228,8 +228,8 @@ opentrons/protocol_api/config.py,sha256=r9lyvXjagTX_g3q5FGURPpcz2IA9sSF7Oa_1mKx-
228
228
  opentrons/protocol_api/create_protocol_context.py,sha256=wwsZje0L__oDnu1Yrihau320_f-ASloR9eL1QCtkOh8,7612
229
229
  opentrons/protocol_api/deck.py,sha256=94vFceg1SC1bAGd7TvC1ZpYwnJR-VlzurEZ6jkacYeg,8910
230
230
  opentrons/protocol_api/disposal_locations.py,sha256=NRiSGmDR0LnbyEkWSOM-o64uR2fUoB1NWJG7Y7SsJSs,7920
231
- opentrons/protocol_api/instrument_context.py,sha256=t6axAJrF-0CajwWeTqKNObI7huMSx5YKibbsfyITWeI,117544
232
- opentrons/protocol_api/labware.py,sha256=X1SP7BGPG7Nqk7HSZTZoVhHDk0brgqA2_eGZKjPbwlY,61546
231
+ opentrons/protocol_api/instrument_context.py,sha256=XUVIxnylpkhviFy5jxYJv-o7Lvmb2FJIbdoEjQ_9Vro,117886
232
+ opentrons/protocol_api/labware.py,sha256=1m1y7h70bBBqW60LjiCPQWwFfVXzY_goJrB773yUN0A,60407
233
233
  opentrons/protocol_api/module_contexts.py,sha256=3tVXj6Q7n-WuTJPU_dvIQLzzGv1P-jsMuDtMVpuhAf8,48291
234
234
  opentrons/protocol_api/module_validation_and_errors.py,sha256=XL_m72P8rcvGO2fynY7UzXLcpGuI6X4s0V6Xf735Iyc,1464
235
235
  opentrons/protocol_api/protocol_context.py,sha256=CHMG5xbx_wxHDQYcobOWo2E9vsRw7FPSNy9J3YV99fM,66126
@@ -238,7 +238,7 @@ opentrons/protocol_api/validation.py,sha256=uiVTHyJF3wSh5LLfaIDBTELoMNCAT17E767u
238
238
  opentrons/protocol_api/core/__init__.py,sha256=-g74o8OtBB0LmmOvwkRvPgrHt7fF7T8FRHDj-x_-Onk,736
239
239
  opentrons/protocol_api/core/common.py,sha256=q9ZbfpRdBvB3iDAOCyONtupvkYP5n1hjE-bwqGcwP_U,1172
240
240
  opentrons/protocol_api/core/core_map.py,sha256=gq3CIYPxuPvozf8yj8FprqBfs3e4ZJGQ6s0ViPbwV08,1757
241
- opentrons/protocol_api/core/instrument.py,sha256=SzhVUR_ZdLfACs8blxKoJHU5aKwwoCBC2UZ07Hpmrvg,13570
241
+ opentrons/protocol_api/core/instrument.py,sha256=MQHI1_MkrtrKM9lN-7BsVQ-xNc4on39TRr8M3HB3WPY,13705
242
242
  opentrons/protocol_api/core/labware.py,sha256=-ZOjkalikXCV3ptehKCNaWGAdKxIdwne8LRFQW9NAm4,4290
243
243
  opentrons/protocol_api/core/module.py,sha256=z2STDyqqxZX3y6UyJVDnajeFXMEn1ie2NRBYHhry_XE,13838
244
244
  opentrons/protocol_api/core/protocol.py,sha256=v7v28jfeHSfOf-tqFDW2chGtrEatPiZ1y6YNwHfmtAs,9058
@@ -248,7 +248,7 @@ opentrons/protocol_api/core/well_grid.py,sha256=BU28DKaBgEU_JdZ6pEzrwNxmuh6TkO4z
248
248
  opentrons/protocol_api/core/engine/__init__.py,sha256=B_5T7zgkWDb1mXPg4NbT-wBkQaK-WVokMMnJRNu7xiM,582
249
249
  opentrons/protocol_api/core/engine/deck_conflict.py,sha256=q3JViIAHDthIqq6ce7h2gxw3CHRfYsm5kkwzuXB-Gnc,12334
250
250
  opentrons/protocol_api/core/engine/exceptions.py,sha256=aZgNrmYEeuPZm21nX_KZYtvyjv5h_zPjxxgPkEV7_bw,725
251
- opentrons/protocol_api/core/engine/instrument.py,sha256=gvCUJo7Oy1KL7VUBeIQQedxFdTFFRhQGGeH-VOCtMuk,95899
251
+ opentrons/protocol_api/core/engine/instrument.py,sha256=qkoNv0KO_jQZffQDV4ixU2r-CkLIJl5IOz1dgs1G5V8,95869
252
252
  opentrons/protocol_api/core/engine/labware.py,sha256=1xvzguNnK7aecFLiJK0gtRrZ5kpwtzLS73HnKvdJ5lc,8413
253
253
  opentrons/protocol_api/core/engine/load_labware_params.py,sha256=I4Cb8rqpBhmykQuZE8QRG802APrdCy_TYS88rm_9oGA,7159
254
254
  opentrons/protocol_api/core/engine/module_core.py,sha256=MLPgYSRJHUZPZ9rTLvsg3GlpL5b6-Pjk5UBgXCGrL6U,30994
@@ -258,12 +258,12 @@ opentrons/protocol_api/core/engine/point_calculations.py,sha256=C2eF0fvJQGMqQv3D
258
258
  opentrons/protocol_api/core/engine/protocol.py,sha256=_1gdg4lq2B21LWV9Tqb924E39HCPCgozAHxaCRGDSIk,46759
259
259
  opentrons/protocol_api/core/engine/robot.py,sha256=o252HrC11tmZ5LRKT6NwXCoTeqcQFXHeNjszfxbJHjo,5366
260
260
  opentrons/protocol_api/core/engine/stringify.py,sha256=GwFgEhFMk-uPfFQhQG_2mkaf4cxaItiY8RW7rZwiooQ,2794
261
- opentrons/protocol_api/core/engine/transfer_components_executor.py,sha256=2fO_SFt4Uf7Z5Byo8bJLcrieTO1DnNqbfq5ASY_Wyas,36644
261
+ opentrons/protocol_api/core/engine/transfer_components_executor.py,sha256=96SsftBRPB5WCeChLokXkdPJcmjP8FqlXEZXhNAlZKA,37582
262
262
  opentrons/protocol_api/core/engine/well.py,sha256=PEtDwdC8NkHjqasJaDaVDtzc_WFw-qv5Lf7IU1DkrLY,7570
263
263
  opentrons/protocol_api/core/legacy/__init__.py,sha256=_9jCJNKG3SlS_vljVu8HHkZmtLf4F-f-JHALLF5d5go,401
264
264
  opentrons/protocol_api/core/legacy/deck.py,sha256=qHqcGo-Kdkl9L1aOE0pwrm9tsAnwkXbt4rIOr_VEP-s,13955
265
265
  opentrons/protocol_api/core/legacy/labware_offset_provider.py,sha256=2DLIby9xmUrwLb2ht8hZbvNTxqPhNzWijd7yCb2cqP8,3783
266
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py,sha256=kX5QD9aeWv-ytHASwTqPUZyaiwtczWSnKwsygAJZIjM,26282
266
+ opentrons/protocol_api/core/legacy/legacy_instrument_core.py,sha256=k-aM8Eu4qHqfhb8iRyn3jn3dZOGiq8JzQes8YUwY2v4,26501
267
267
  opentrons/protocol_api/core/legacy/legacy_labware_core.py,sha256=WQOgtMlq--zv0Ch7mmraYr9rQBT4ie2zHqwgamBq9J8,8606
268
268
  opentrons/protocol_api/core/legacy/legacy_module_core.py,sha256=tUhj88NKBMjCmCg6wjh1e2HX4d5hxjh8ZeJiYXaTaGY,23111
269
269
  opentrons/protocol_api/core/legacy/legacy_protocol_core.py,sha256=ZIFC7W6YA61oWWkq5xYGTcI_2S2pmALz16uB1R8HVyQ,23670
@@ -272,7 +272,7 @@ opentrons/protocol_api/core/legacy/load_info.py,sha256=r-WaH5ZJb3TRCp_zvbMMh0P4B
272
272
  opentrons/protocol_api/core/legacy/module_geometry.py,sha256=lvWFHZ81-JFw-1VZUW1R3yUIb59xpXT6H3jwlRintRo,21082
273
273
  opentrons/protocol_api/core/legacy/well_geometry.py,sha256=n5bEsvYZXXTAqYSAqlXd5t40bUPPrJ2Oj2frBZafQHA,4719
274
274
  opentrons/protocol_api/core/legacy_simulator/__init__.py,sha256=m9bLHGDJ6LSYC2WPm8tpOuu0zWSOPIrlybQgjRQBw9k,647
275
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py,sha256=vScFZnEGswGLeIHPLGyJraeNoM3CJEeZ9ED4Puf7V1Y,22762
275
+ opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py,sha256=pV-PEw8jMcdXMi2U5yplIkFJUUlugXd7OMJl_riiVf4,22940
276
276
  opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py,sha256=28HrrHzeUfnGKXpZqQ-VM8WbPiadqVhKj2S9y33q6Lo,2910
277
277
  opentrons/protocol_engine/__init__.py,sha256=UPSk7MbidkiSH_h4V3yxMvyTePKpRr5DM9-wfkJrlSo,4094
278
278
  opentrons/protocol_engine/create_protocol_engine.py,sha256=tfDIsC7_JKlRiCXPB_8tuxRsssU6o0ViRmWbGPtX9QA,7582
@@ -294,7 +294,7 @@ opentrons/protocol_engine/commands/__init__.py,sha256=b073p4seq9bnyqMydVrYl9b_yC
294
294
  opentrons/protocol_engine/commands/air_gap_in_place.py,sha256=Z1Tz2wFtEnlJBf_0xW0tEvX1yYJbA8ZmdZcHG_YIKwE,5387
295
295
  opentrons/protocol_engine/commands/aspirate.py,sha256=ZxpwQ5Zq-AS11aNfgxx6PsL_MrBEaawAxAi7jWwpIRE,7920
296
296
  opentrons/protocol_engine/commands/aspirate_in_place.py,sha256=vJiLSZqEzuMj-kuQESZevHM5g9brXAo159GhaFyEQm8,6530
297
- opentrons/protocol_engine/commands/aspirate_while_tracking.py,sha256=PTTqg-rEEzq43ukSdA8XiMxF1wY55MR6YR7bXqpZooo,8378
297
+ opentrons/protocol_engine/commands/aspirate_while_tracking.py,sha256=se1GIJWxxVCmvYQF_nhJK11D54KKeoN5Yne5Sti93-A,7224
298
298
  opentrons/protocol_engine/commands/blow_out.py,sha256=3gboq4x5S8fq7j4ZZGNClXFDlOjcdW1v2g58GPdhaPI,4338
299
299
  opentrons/protocol_engine/commands/blow_out_in_place.py,sha256=jm2XXyJfIP9-AAFwXhD59_13nX18-i6QqpLGb-lK7sI,3391
300
300
  opentrons/protocol_engine/commands/command.py,sha256=1hWH_KWg_WDL4R4VXe1ZH2vO6pYt5SA-ZpuPPF1oV5E,10149
@@ -305,7 +305,7 @@ opentrons/protocol_engine/commands/configure_nozzle_layout.py,sha256=M_s5Ee03a7s
305
305
  opentrons/protocol_engine/commands/custom.py,sha256=vOJc7QSNnYTpLvJm98OfDKjgcvVFRZs1eEKEd9WkPN0,2157
306
306
  opentrons/protocol_engine/commands/dispense.py,sha256=gmjBXgGuWhB-SEUboXiNHqkaUrmpRTqSN4Dy362ln8w,6444
307
307
  opentrons/protocol_engine/commands/dispense_in_place.py,sha256=gcj0HXUkPrU3Qz_DbWzP3XZHuB8tXSMTo9CFoGi25lw,6263
308
- opentrons/protocol_engine/commands/dispense_while_tracking.py,sha256=4AU1W0f6W4W_MgvaCQk2xQXkZQRZqMIHvWoZTEEtnik,8227
308
+ opentrons/protocol_engine/commands/dispense_while_tracking.py,sha256=vn0nw5D4ggpTEwarConFPHUVI4gNShehs5v1U5Kn9sY,6644
309
309
  opentrons/protocol_engine/commands/drop_tip.py,sha256=ZZ63IoiT4dgWcemAHhNQfV4DUhkl-ToJyTRTxIiyAkc,7895
310
310
  opentrons/protocol_engine/commands/drop_tip_in_place.py,sha256=gwSNEKBwds7kOTucXKSK74ozrDe7Cqhta7NR6IqKV3g,7062
311
311
  opentrons/protocol_engine/commands/generate_command_schema.py,sha256=21Al_XQyRMNTb2ssVaxcNSPlgreOsCtKVXf8kZgpvR4,2296
@@ -418,7 +418,7 @@ opentrons/protocol_engine/execution/hardware_stopper.py,sha256=wlIl7U3gvnOiCvwri
418
418
  opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py,sha256=BSFLzSSeELAYZCrCUfJZx5DdlrwU06Ur92TYd0T-hzM,9084
419
419
  opentrons/protocol_engine/execution/labware_movement.py,sha256=XYVcxZOQ6_udpxcpwkIJpVD8-lgWLhizJAcRD9BclIo,12247
420
420
  opentrons/protocol_engine/execution/movement.py,sha256=AWcj7xlrOh3QrvoaH0sZO63yrPCEc7VE8hKfJNxxtP0,12527
421
- opentrons/protocol_engine/execution/pipetting.py,sha256=PSWU3c1woM4lPmMGktvq30a3BlhXtho9UrJv80e32ik,22043
421
+ opentrons/protocol_engine/execution/pipetting.py,sha256=0_OAxIDCQBAtdNOvdYwV0kEYfJvUrJWr-63rocqf75E,22094
422
422
  opentrons/protocol_engine/execution/queue_worker.py,sha256=riVVywKIOQ3Lx-woFuuSqqBtfeKFt23nCUnsk7gSVoI,2860
423
423
  opentrons/protocol_engine/execution/rail_lights.py,sha256=eiJT6oI_kFk7rFuFkZzISZiLNnpf7Kkh86Kyk9wQ_Jo,590
424
424
  opentrons/protocol_engine/execution/run_control.py,sha256=ksvI2zkguC4G3lR3HJgAF8uY1PNcaRfi7UOYu-oIZgo,1144
@@ -580,11 +580,12 @@ opentrons/util/entrypoint_util.py,sha256=C0KN-09_WgNkqLbCyIB3yVm-kJoe7RGrZTb7qh9
580
580
  opentrons/util/get_union_elements.py,sha256=H1KqLnG1zYvI2kanhc3MXRZT-S07E5a2vF1jEkhXpCs,1073
581
581
  opentrons/util/helpers.py,sha256=3hr801bWGbxEcOFAS7f-iOhmnUhoK5qahbB8SIvaCfY,165
582
582
  opentrons/util/linal.py,sha256=IlKAP9HkNBBgULeSf4YVwSKHdx9jnCjSr7nvDvlRALg,5753
583
- opentrons/util/logging_config.py,sha256=UHoY7dyD6WMYNP5GKowbMxUSG_hlXeI5TaIwksR5u-U,6887
583
+ opentrons/util/logging_config.py,sha256=7et4YYuQdWdq_e50U-8vFS_QyNBRgdnqPGAQJm8qrIo,9954
584
+ opentrons/util/logging_queue_handler.py,sha256=ZsSJwy-oV8DXwpYiZisQ1PbYwmK2cOslD46AcyJ1E4I,2484
584
585
  opentrons/util/performance_helpers.py,sha256=ew7H8XD20iS6-2TJAzbQeyzStZkkE6PzHt_Adx3wbZQ,5172
585
- opentrons-8.4.0a4.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
586
- opentrons-8.4.0a4.dist-info/METADATA,sha256=yXaEzAGJCCs5doztDC3sU4iNZ72LUclkvtfeHAQ3G0I,5084
587
- opentrons-8.4.0a4.dist-info/WHEEL,sha256=qUzzGenXXuJTzyjFah76kDVqDvnk-YDzY00svnrl84w,109
588
- opentrons-8.4.0a4.dist-info/entry_points.txt,sha256=fTa6eGCYkvOtv0ov-KVE8LLGetgb35LQLF9x85OWPVw,106
589
- opentrons-8.4.0a4.dist-info/top_level.txt,sha256=wk6whpbMZdBQpcK0Fg0YVfUGrAgVOFON7oQAhOMGMW8,10
590
- opentrons-8.4.0a4.dist-info/RECORD,,
586
+ opentrons-8.4.0a5.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
587
+ opentrons-8.4.0a5.dist-info/METADATA,sha256=dVv_Zex7NX_Yf6tBwmhxcaTUWKOKDvYAmuqkq99qwEU,5084
588
+ opentrons-8.4.0a5.dist-info/WHEEL,sha256=qUzzGenXXuJTzyjFah76kDVqDvnk-YDzY00svnrl84w,109
589
+ opentrons-8.4.0a5.dist-info/entry_points.txt,sha256=fTa6eGCYkvOtv0ov-KVE8LLGetgb35LQLF9x85OWPVw,106
590
+ opentrons-8.4.0a5.dist-info/top_level.txt,sha256=wk6whpbMZdBQpcK0Fg0YVfUGrAgVOFON7oQAhOMGMW8,10
591
+ opentrons-8.4.0a5.dist-info/RECORD,,