Skip to content

Commit

Permalink
feat(api): JSON protocol pipette loading support in Protocol Engine (#…
Browse files Browse the repository at this point in the history
…7766)

* LoadPipetteRequest has an optional pipetteId which will override the id generation.

* support pipetteId arg for LoadPipetteRequest.

* SyncClient.load_pipette does not need pipette_id argument.

closes #7430
  • Loading branch information
amitlissack authored May 4, 2021
1 parent 2f2a558 commit 7aa9034
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 6 deletions.
9 changes: 9 additions & 0 deletions api/src/opentrons/protocol_engine/commands/load_pipette.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"""Load pipette command request, result, and implementation models."""
from __future__ import annotations

from typing import Optional

from pydantic import BaseModel, Field

from opentrons.types import MountType
Expand All @@ -19,6 +22,11 @@ class LoadPipetteRequest(BaseModel):
...,
description="The mount the pipette should be present on.",
)
pipetteId: Optional[str] = Field(
None,
description="An optional ID to assign to this pipette. If None, an ID "
"will be generated."
)

def get_implementation(self) -> LoadPipetteImplementation:
"""Get the load pipette request's command implementation."""
Expand All @@ -44,6 +52,7 @@ async def execute(self, handlers: CommandHandlers) -> LoadPipetteResult:
loaded_pipette = await handlers.equipment.load_pipette(
pipette_name=self._request.pipetteName,
mount=self._request.mount,
pipette_id=self._request.pipetteId
)

return LoadPipetteResult(pipetteId=loaded_pipette.pipette_id)
18 changes: 15 additions & 3 deletions api/src/opentrons/protocol_engine/execution/equipment.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Equipment command side-effect logic."""
from dataclasses import dataclass
from typing import Tuple
from typing import Tuple, Optional

from opentrons_shared_data.labware.dev_types import LabwareDefinition
from opentrons.types import MountType
Expand Down Expand Up @@ -77,8 +77,19 @@ async def load_pipette(
self,
pipette_name: PipetteName,
mount: MountType,
pipette_id: Optional[str],
) -> LoadedPipette:
"""Ensure the requested pipette is attached."""
"""Ensure the requested pipette is attached.
Args:
pipette_name: The pipette name.
mount: The mount on which pipette must be attached.
pipette_id: An optional identifier to assign the pipette. If None, an
identifier will be generated.
Returns:
A LoadedPipette object.
"""
other_mount = mount.other_mount()
other_pipette = self._state.pipettes.get_pipette_data_by_mount(
other_mount,
Expand All @@ -97,6 +108,7 @@ async def load_pipette(
except RuntimeError as e:
raise FailedToLoadPipetteError(str(e)) from e

pipette_id = self._resources.id_generator.generate_id()
pipette_id = pipette_id if pipette_id is not None else \
self._resources.id_generator.generate_id()

return LoadedPipette(pipette_id=pipette_id)
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def test_load_pipette_request() -> None:

assert request.pipetteName == "p300_single"
assert request.mount == MountType.LEFT
assert request.pipetteId is None


def test_load_pipette_result() -> None:
Expand All @@ -37,6 +38,7 @@ async def test_load_pipette_implementation(mock_handlers: AsyncMock) -> None:
request = LoadPipetteRequest(
pipetteName=PipetteName.P300_SINGLE,
mount=MountType.LEFT,
pipetteId="some id"
)
impl = request.get_implementation()
result = await impl.execute(mock_handlers)
Expand All @@ -45,4 +47,5 @@ async def test_load_pipette_implementation(mock_handlers: AsyncMock) -> None:
mock_handlers.equipment.load_pipette.assert_called_with(
pipette_name="p300_single",
mount=MountType.LEFT,
pipette_id="some id",
)
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,29 @@ async def test_load_pipette_assigns_id(
res = await handler.load_pipette(
pipette_name=PipetteName.P300_SINGLE,
mount=MountType.LEFT,
pipette_id=None,
)

assert type(res) == LoadedPipette
assert res.pipette_id == "unique-id"


async def test_load_pipette_uses_provided_id(
mock_resources_with_data: AsyncMock,
handler: EquipmentHandler,
) -> None:
"""It should use the provided ID rather than generating an ID for the pipette."""
res = await handler.load_pipette(
pipette_name=PipetteName.P300_SINGLE,
mount=MountType.LEFT,
pipette_id="my pipette id"
)

assert type(res) == LoadedPipette
assert res.pipette_id == "my pipette id"
mock_resources_with_data.id_generator.generate_id.assert_not_called()


async def test_load_pipette_checks_checks_existence(
mock_state_view: MagicMock,
mock_hardware: AsyncMock,
Expand All @@ -126,6 +143,7 @@ async def test_load_pipette_checks_checks_existence(
await handler.load_pipette(
pipette_name=PipetteName.P300_SINGLE,
mount=MountType.LEFT,
pipette_id=None,
)

mock_state_view.pipettes.get_pipette_data_by_mount.assert_called_with(
Expand All @@ -151,6 +169,7 @@ async def test_load_pipette_checks_checks_existence_with_already_loaded(
await handler.load_pipette(
pipette_name=PipetteName.P300_SINGLE,
mount=MountType.RIGHT,
pipette_id=None,
)

mock_state_view.pipettes.get_pipette_data_by_mount.assert_called_with(
Expand Down Expand Up @@ -181,4 +200,5 @@ async def test_load_pipette_raises_if_pipette_not_attached(
await handler.load_pipette(
pipette_name=PipetteName.P300_SINGLE,
mount=MountType.LEFT,
pipette_id=None,
)
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ stages:
data: &create_pipette_data
pipetteName: p300_single
mount: right
pipetteId: null
response:
status_code: 200
json:
Expand All @@ -100,6 +101,31 @@ stages:
completedAt: !anystr
result:
pipetteId: !anystr
- name: Load pipette command specifying the id
request:
url: "{host:s}:{port:d}/sessions/{session_id}/commands/execute"
method: POST
json:
data:
command: equipment.loadPipette
data: &create_pipette_data_with_id
pipetteName: p10_single
mount: left
pipetteId: my pipette
response:
status_code: 200
json:
links: !anydict
data:
id: !anystr
data: *create_pipette_data_with_id
command: equipment.loadPipette
status: executed
createdAt: !anystr
startedAt: !anystr
completedAt: !anystr
result:
pipetteId: my pipette
- name: Delete the session
request:
url: "{host:s}:{port:d}/sessions/{session_id}"
Expand Down
7 changes: 4 additions & 3 deletions robot-server/tests/service/session/models/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,16 @@ def test_not_empty():
"command": "equipment.loadPipette",
"data": {
"pipetteName": "p10_single",
"mount": "left"
"mount": "left",
"pipetteId": None,
}
}
})
assert request.data.command == \
command_definitions.EquipmentCommand.load_pipette
assert request.data.data == pe_commands.LoadPipetteRequest(
pipetteName="p10_single",
mount=MountType.LEFT
mount=MountType.LEFT,
)

dt = datetime(2000, 1, 1)
Expand All @@ -81,7 +82,7 @@ def test_not_empty():
command_definitions.EquipmentCommand.load_pipette
assert response.data == pe_commands.LoadPipetteRequest(
pipetteName="p10_single",
mount=MountType.LEFT
mount=MountType.LEFT,
)
assert response.id == "id"
assert response.createdAt == dt
Expand Down

0 comments on commit 7aa9034

Please sign in to comment.