Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(api, robot-server): Refactor ProtocolRunner per protocol type #12343

Merged
merged 36 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
2410782
WIP: created an abstract interface for ProtocolRunner. started JsonRu…
TamarZanzouri Mar 14, 2023
290e860
WIP create_protocol_runner
TamarZanzouri Mar 16, 2023
842a99e
test json_runner and finished logic for json_runner
TamarZanzouri Mar 17, 2023
f5b5325
WIP python and legacy runner
TamarZanzouri Mar 17, 2023
7de1dae
python and legacy runner implementation
TamarZanzouri Mar 20, 2023
8f30fa7
create_simulating_runner.py
TamarZanzouri Mar 20, 2023
c73922c
fixed robot server dependencies.py
TamarZanzouri Mar 21, 2023
33c260a
MaintenanceRunner
TamarZanzouri Mar 21, 2023
b727804
wip robot-server
TamarZanzouri Mar 21, 2023
5ecd3cb
changed create_simulating_runner to monkeypatch
TamarZanzouri Mar 22, 2023
037f322
removed set_run_started_at and fixed test failing on started_at
TamarZanzouri Mar 22, 2023
d54c1ec
rollback depends for protocol_analyzer.py and moved runner creation i…
TamarZanzouri Mar 22, 2023
af5aaac
reorganize prtocol_runner tests and text fixes
TamarZanzouri Mar 23, 2023
3a870b0
removed task_queue from maintenance runner
TamarZanzouri Mar 23, 2023
4009a37
Merge branch 'edge' into RSS-200-refactor-protocol-runner
TamarZanzouri Mar 23, 2023
50978e3
linting
TamarZanzouri Mar 23, 2023
20d3c3f
fixed failing test in actions router
TamarZanzouri Mar 23, 2023
08524fd
fixed maintenance test with task runner
TamarZanzouri Mar 23, 2023
da0fdba
Update api/src/opentrons/protocol_runner/protocol_runner.py
TamarZanzouri Mar 27, 2023
b38aaf7
pr feedback
TamarZanzouri Mar 27, 2023
14e2fa0
changed ProtocolRunResult to RunnerRunResult
TamarZanzouri Mar 27, 2023
5aa7899
added linting ignore
TamarZanzouri Mar 28, 2023
f36be33
fixed g-code testing
TamarZanzouri Mar 28, 2023
a81df1d
g-code testing fix
TamarZanzouri Mar 28, 2023
b647315
linting
TamarZanzouri Mar 28, 2023
e802d65
linting
TamarZanzouri Mar 28, 2023
923e00c
added pe.play for run live commands
TamarZanzouri Mar 28, 2023
e1e9ddd
added tavern test for posting protocol commands
TamarZanzouri Mar 29, 2023
88cae89
addressed PR comments, fixed live runner bug, updated tests
sanni-t Apr 5, 2023
742e292
updated tests, fixed linter errors
sanni-t Apr 5, 2023
93be36f
Merge branch 'edge' into RSS-200-refactor-protocol-runner
sanni-t Apr 7, 2023
56a1b2f
correct docstring
sanni-t Apr 7, 2023
b272e10
Apply suggestions from code review
sanni-t Apr 13, 2023
433ca47
fix runner smoke tests and linter errors, some renaming from PR review
sanni-t Apr 14, 2023
b6e7385
update runner typing & type naming, renamed set_task_queue_wait() to …
sanni-t Apr 14, 2023
5789c41
address all remaining review comments
sanni-t Apr 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
pr feedback
  • Loading branch information
TamarZanzouri committed Mar 27, 2023
commit b38aaf742afb83893334142cd9a73b07f49c5001
10 changes: 5 additions & 5 deletions api/src/opentrons/protocol_runner/__init__.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
"""Protocol run control and management.

The main export of this module is the ProtocolRunner class. See
The main export of this module is the AbstractRunner class. See
protocol_runner.py for more details.
"""
from .protocol_runner import (
ProtocolRunner,
AbstractRunner,
ProtocolRunResult,
create_protocol_runner,
JsonRunner,
PythonAndLegacyRunner,
MaintenanceRunner,
LiveRunner,
)
from .create_simulating_runner import create_simulating_runner

__all__ = [
"ProtocolRunner",
"AbstractRunner",
"ProtocolRunResult",
"create_simulating_runner",
"create_protocol_runner",
"JsonRunner",
"PythonAndLegacyRunner",
"MaintenanceRunner",
"LiveRunner",
]
12 changes: 6 additions & 6 deletions api/src/opentrons/protocol_runner/create_simulating_runner.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Simulating ProtocolRunner factory."""
"""Simulating AbstractRunner factory."""

from opentrons.config import feature_flags
from opentrons.hardware_control import API as OT2API, HardwareControlAPI
Expand All @@ -11,13 +11,13 @@
from opentrons_shared_data.robot.dev_types import RobotType

from .legacy_wrappers import LegacySimulatingContextCreator
from .protocol_runner import create_protocol_runner, ProtocolRunner
from .protocol_runner import create_protocol_runner, AbstractRunner


async def create_simulating_runner(
robot_type: RobotType, protocol_config: ProtocolConfig
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See if accepting a protocol_type makes more sense here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a TODO to address later.

) -> ProtocolRunner:
"""Create a ProtocolRunner wired to a simulating HardwareControlAPI.
) -> AbstractRunner:

This comment was marked as resolved.

"""Create a AbstractRunner wired to a simulating HardwareControlAPI.

Example:
```python
Expand All @@ -27,15 +27,15 @@ async def create_simulating_runner(
from opentrons.protocol_runner import (
ProtocolType,
ProtocolFile,
ProtocolRunner,
AbstractRunner,
create_simulating_runner,
)

protocol = ProtocolFile(
protocol_type=ProtocolType.PYTHON,
files=[Path("/path/to/protocol.py")],
)
runner: ProtocolRunner = await create_simulating_runner()
runner: AbstractRunner = await create_simulating_runner()
commands: List[Command] = await runner.run(protocol)
```
"""
Expand Down
79 changes: 21 additions & 58 deletions api/src/opentrons/protocol_runner/protocol_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import asyncio
from typing import List, NamedTuple, Optional, Union

from typing_extensions import Protocol as TypingProtocol
from abc import ABC, abstractmethod

import anyio

Expand Down Expand Up @@ -38,48 +38,54 @@ class ProtocolRunResult(NamedTuple):
state_summary: StateSummary


class ProtocolRunner(TypingProtocol):
class AbstractRunner(ABC):

This comment was marked as outdated.

"""An interface to manage and control a protocol run.

The ProtocolRunner is primarily responsible for feeding a ProtocolEngine
The AbstractRunner is primarily responsible for feeding a ProtocolEngine
with commands and control signals. These commands and signals are
generated by protocol files, hardware signals, or externally via

This comment was marked as outdated.

the HTTP robot-server.

A ProtocolRunner controls a single run. Once the run is finished,
you will need a new ProtocolRunner to do another run.
A AbstractRunner controls a single run. Once the run is finished,
you will need a new AbstractRunner to do another run.
"""

@abstractmethod
def was_started(self) -> bool:
"""Whether the runner has been started.

This value is latched; once it is True, it will never become False.
"""

@abstractmethod
async def load(self, protocol_source: ProtocolSource) -> None:
"""Load a ProtocolSource into managed ProtocolEngine.

Calling this method is only necessary if the runner will be used
to control the run of a file-based protocol.
"""

@abstractmethod
def play(self) -> None:
"""Start or resume the run."""

@abstractmethod
def pause(self) -> None:
"""Pause the run."""

@abstractmethod
async def stop(self) -> None:
"""Stop (cancel) the run."""

@abstractmethod
async def run(
self,
protocol_source: Optional[ProtocolSource] = None,
) -> ProtocolRunResult:
"""Run a given protocol to completion."""


class PythonAndLegacyRunner(ProtocolRunner):
class PythonAndLegacyRunner(AbstractRunner):
"""Protocol runner implementation for Python protocols, and JSON protocols ≤v5."""

def __init__(
Expand All @@ -105,18 +111,9 @@ def __init__(
self._task_queue = task_queue or TaskQueue(cleanup_func=protocol_engine.finish)

def was_started(self) -> bool:
"""Whether the runner has been started.

This value is latched; once it is True, it will never become False.
"""
return self._protocol_engine.state_view.commands.has_been_played()

async def load(self, protocol_source: ProtocolSource) -> None:
"""Load a ProtocolSource into managed ProtocolEngine.

Calling this method is only necessary if the runner will be used
to control the run of a file-based protocol.
"""
labware_definitions = await protocol_reader.extract_labware_definitions(
protocol_source=protocol_source
)
Expand Down Expand Up @@ -152,15 +149,12 @@ async def load(self, protocol_source: ProtocolSource) -> None:
)

def play(self) -> None:
"""Start or resume the run."""
self._protocol_engine.play()

def pause(self) -> None:
"""Pause the run."""
self._protocol_engine.pause()

async def stop(self) -> None:
"""Stop (cancel) the run."""
if self.was_started():
await self._protocol_engine.stop()
else:
Expand All @@ -173,11 +167,10 @@ async def run(
self,
protocol_source: Optional[ProtocolSource] = None,
) -> ProtocolRunResult:
"""Run a given protocol to completion."""
# TODO(mc, 2022-01-11): move load to runner creation, remove from `run`
# currently `protocol_source` arg is only used by tests
if protocol_source:
await self.load(protocol_source)
await self.load(protocol_source=protocol_source)

await self._hardware_api.home()
self.play()
Expand All @@ -189,7 +182,7 @@ async def run(
return ProtocolRunResult(commands=commands, state_summary=run_data)


class JsonRunner(ProtocolRunner):
class JsonRunner(AbstractRunner):
"""Protocol runner implementation for json protocols."""

def __init__(
Expand All @@ -210,18 +203,9 @@ def __init__(
self._task_queue = task_queue or TaskQueue(cleanup_func=protocol_engine.finish)

def was_started(self) -> bool:
"""Whether the runner has been started.

This value is latched; once it is True, it will never become False.
"""
return self._protocol_engine.state_view.commands.has_been_played()

async def load(self, protocol_source: ProtocolSource) -> None:
"""Load a ProtocolSource into managed ProtocolEngine.

Calling this method is only necessary if the runner will be used
to control the run of a file-based protocol.
"""
labware_definitions = await protocol_reader.extract_labware_definitions(
protocol_source=protocol_source
)
Expand Down Expand Up @@ -267,15 +251,12 @@ async def load(self, protocol_source: ProtocolSource) -> None:
self._task_queue.set_run_func(func=self._protocol_engine.wait_until_complete)

def play(self) -> None:
"""Start or resume the run."""
self._protocol_engine.play()

def pause(self) -> None:
"""Pause the run."""
self._protocol_engine.pause()

async def stop(self) -> None:
"""Stop (cancel) the run."""
if self.was_started():
await self._protocol_engine.stop()
else:
Expand All @@ -288,7 +269,6 @@ async def run(
self,
protocol_source: Optional[ProtocolSource] = None,
) -> ProtocolRunResult:
"""Run a given protocol to completion."""
# TODO(mc, 2022-01-11): move load to runner creation, remove from `run`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can totally do this for this pr

# currently `protocol_source` arg is only used by tests
if protocol_source:
Expand All @@ -304,51 +284,35 @@ async def run(
return ProtocolRunResult(commands=commands, state_summary=run_data)


class MaintenanceRunner(ProtocolRunner):
class LiveRunner(AbstractRunner):
"""Protocol runner implementation for setup commands."""
sanni-t marked this conversation as resolved.
Show resolved Hide resolved

def __init__(
self,
protocol_engine: ProtocolEngine,
hardware_api: HardwareControlAPI,
) -> None:
"""Initialize the MaintenanceRunner with its dependencies."""
"""Initialize the LiveRunner with its dependencies."""
self._protocol_engine = protocol_engine
# TODO(mc, 2022-01-11): replace task queue with specific implementations
# of runner interface
self._hardware_api = hardware_api

def was_started(self) -> bool:
"""Whether the runner has been started.

This value is latched; once it is True, it will never become False.
"""
return self._protocol_engine.state_view.commands.has_been_played()

async def load(self, protocol_source: ProtocolSource) -> None:
"""Load a ProtocolSource into managed ProtocolEngine.

Calling this method is only necessary if the runner will be used
to control the run of a file-based protocol.
"""
raise NotImplementedError(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raise a more appropriate error

"MaintenanceRunner.load() not supported for setup commands. no protocol associated."
"LiveRunner.load() not supported for setup commands. no protocol associated."
)

def play(self) -> None:
"""Start or resume the run."""
raise NotImplementedError(
"MaintenanceRunner.play() not supported for setup commands."
)
self._protocol_engine.play()

def pause(self) -> None:
"""Pause the run."""
raise NotImplementedError(
"MaintenanceRunner.pause() not supported for setup commands."
)
self._protocol_engine.pause()

async def stop(self) -> None:
"""Stop (cancel) the run."""
if self.was_started():
await self._protocol_engine.stop()
else:
Expand All @@ -361,7 +325,6 @@ async def run(
self,
protocol_source: Optional[ProtocolSource] = None,
SyntaxColoring marked this conversation as resolved.
Show resolved Hide resolved
) -> ProtocolRunResult:
"""Run a given protocol to completion."""
if protocol_source:
TamarZanzouri marked this conversation as resolved.
Show resolved Hide resolved
self.load(protocol_source)

Expand All @@ -382,7 +345,7 @@ def create_protocol_runner(
legacy_file_reader: Optional[LegacyFileReader] = None,
legacy_context_creator: Optional[LegacyContextCreator] = None,
legacy_executor: Optional[LegacyExecutor] = None,
) -> ProtocolRunner:
) -> AbstractRunner:
"""Create a protocol runner."""
if protocol_config:
if (
Expand All @@ -406,7 +369,7 @@ def create_protocol_runner(
legacy_executor=legacy_executor,
)

return MaintenanceRunner(
return LiveRunner(
protocol_engine=protocol_engine,
hardware_api=hardware_api,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Smoke tests for the ProtocolRunner and ProtocolEngine classes.
"""Smoke tests for the AbstractRunner and ProtocolEngine classes.

This comment was marked as resolved.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an integration test for LiveRunner which covers the requirement.


These tests construct a ProtocolRunner with a real ProtocolEngine
These tests construct a AbstractRunner with a real ProtocolEngine
hooked to a simulating HardwareAPI.

Minimal, but valid and complete, protocol files are then loaded from
Expand Down Expand Up @@ -43,7 +43,7 @@ async def test_runner_with_python(
python_protocol_file: Path,
tempdeck_v1_def: ModuleDefinition,
) -> None:
"""It should run a Python protocol on the ProtocolRunner."""
"""It should run a Python protocol on the AbstractRunner."""
sanni-t marked this conversation as resolved.
Show resolved Hide resolved
protocol_reader = ProtocolReader()
protocol_source = await protocol_reader.read_saved(
files=[python_protocol_file],
Expand Down Expand Up @@ -112,7 +112,7 @@ async def test_runner_with_python(


async def test_runner_with_json(json_protocol_file: Path) -> None:
"""It should run a JSON protocol on the ProtocolRunner."""
"""It should run a JSON protocol on the AbstractRunner."""
protocol_reader = ProtocolReader()
protocol_source = await protocol_reader.read_saved(
files=[json_protocol_file],
Expand Down Expand Up @@ -173,7 +173,7 @@ async def test_runner_with_json(json_protocol_file: Path) -> None:


async def test_runner_with_legacy_python(legacy_python_protocol_file: Path) -> None:
"""It should run a Python protocol on the ProtocolRunner."""
"""It should run a Python protocol on the AbstractRunner."""
protocol_reader = ProtocolReader()
protocol_source = await protocol_reader.read_saved(
files=[legacy_python_protocol_file],
Expand Down Expand Up @@ -233,7 +233,7 @@ async def test_runner_with_legacy_python(legacy_python_protocol_file: Path) -> N


async def test_runner_with_legacy_json(legacy_json_protocol_file: Path) -> None:
"""It should run a Python protocol on the ProtocolRunner."""
"""It should run a Python protocol on the AbstractRunner."""
protocol_reader = ProtocolReader()
protocol_source = await protocol_reader.read_saved(
files=[legacy_json_protocol_file],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Tests for the ProtocolRunner's LegacyContextPlugin."""
"""Tests for the AbstractRunner's LegacyContextPlugin."""
sanni-t marked this conversation as resolved.
Show resolved Hide resolved
import inspect
from datetime import datetime
from typing import cast
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Tests for the ProtocolRunner's LegacyContextPlugin."""
"""Tests for the AbstractRunner's LegacyContextPlugin."""
sanni-t marked this conversation as resolved.
Show resolved Hide resolved
import pytest
from anyio import to_thread
from decoy import Decoy, matchers
Expand Down
Loading