-
Notifications
You must be signed in to change notification settings - Fork 176
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
Changes from 1 commit
2410782
290e860
842a99e
f5b5325
7de1dae
8f30fa7
c73922c
33c260a
b727804
5ecd3cb
037f322
d54c1ec
af5aaac
3a870b0
4009a37
50978e3
20d3c3f
08524fd
da0fdba
b38aaf7
14e2fa0
5aa7899
f36be33
a81df1d
b647315
e802d65
923e00c
e1e9ddd
88cae89
742e292
93be36f
56a1b2f
b272e10
433ca47
b6e7385
5789c41
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
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", | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
||
|
@@ -38,48 +38,54 @@ class ProtocolRunResult(NamedTuple): | |
state_summary: StateSummary | ||
|
||
|
||
class ProtocolRunner(TypingProtocol): | ||
class AbstractRunner(ABC): | ||
This comment was marked as outdated.
Sorry, something went wrong. |
||
"""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.
Sorry, something went wrong. |
||
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__( | ||
|
@@ -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 | ||
) | ||
|
@@ -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: | ||
|
@@ -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() | ||
|
@@ -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__( | ||
|
@@ -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 | ||
) | ||
|
@@ -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: | ||
|
@@ -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` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
@@ -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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
@@ -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) | ||
|
||
|
@@ -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 ( | ||
|
@@ -406,7 +369,7 @@ def create_protocol_runner( | |
legacy_executor=legacy_executor, | ||
) | ||
|
||
return MaintenanceRunner( | ||
return LiveRunner( | ||
protocol_engine=protocol_engine, | ||
hardware_api=hardware_api, | ||
) | ||
|
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.
Sorry, something went wrong. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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], | ||
|
@@ -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], | ||
|
@@ -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], | ||
|
@@ -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], | ||
|
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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.