Skip to content

Commit

Permalink
feat(api): Add metadata to session for Python protocols (#2799)
Browse files Browse the repository at this point in the history
* feat(api): Add metadata to session for Python protocols. Closes #2616
  • Loading branch information
btmorr authored Dec 10, 2018
1 parent 68c16c2 commit 1da19bb
Show file tree
Hide file tree
Showing 14 changed files with 153 additions and 8 deletions.
26 changes: 21 additions & 5 deletions api/docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,17 @@ If we were to rewrite this with the Opentrons API, it would look like the follow
# imports
from opentrons import labware, instruments
# metadata
metadata = {
'protocolName': 'My Protocol',
'author': 'Name <[email protected]>',
'description': 'Simple protocol to get started using OT2',
'source': 'Opentrons Protocol Tutorial'
}
# labware
plate = labware.load('96-flat', '2')
tiprack = labware.load('tiprack-200ul', '1')
tiprack = labware.load('opentrons-tiprack-300ul', '1')
# pipettes
pipette = instruments.P300_Single(mount='left', tip_racks=[tiprack])
Expand All @@ -74,12 +82,13 @@ If we were to rewrite this with the Opentrons API, it would look like the follow
How it's Organized
------------------

When writing protocols using the Opentrons API, there are generally three sections:
When writing protocols using the Opentrons API, there are generally five sections:

1) Imports
2) Labware
3) Pipettes
4) Commands
2) Metadata
3) Labware
4) Pipettes
5) Commands

Imports
^^^^^^^
Expand All @@ -93,6 +102,13 @@ From the example above, the "imports" section looked like:
from opentrons import labware, instruments
Metadata
^^^^^^^^

Metadata is a dictionary of data that is read by the server and returned to client applications (such as the Opentrons Run App). It is not needed to run a protocol (and is entirely optional), but if present can help the client application display additional data about the protocol currently being executed.

The fields above ("protocolName", "author", "description", and "source") are the recommended fields, but the metadata dictionary can contain fewer fields, or additional fields as desired (though non-standard fields may not be rendered by the client, depending on how it is designed).

Labware
^^^^^^^

Expand Down
16 changes: 15 additions & 1 deletion api/src/opentrons/api/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def __init__(self, name, text, hardware):
self.instruments = None
self.containers = None
self.modules = None
self.metadata = {}

self.startTime = None

Expand Down Expand Up @@ -210,7 +211,8 @@ async def refresh(self):
# warning/error here if the protocol_text doesn't follow the schema
self._protocol = json.loads(self.protocol_text)
else:
parsed = ast.parse(self.protocol_text)
parsed = ast.parse(self.protocol_text, filename=self.name)
self.metadata = extract_metadata(parsed)
self._protocol = compile(parsed, filename=self.name, mode='exec')
commands = await self._simulate()
self.commands = tree.from_list(commands)
Expand Down Expand Up @@ -347,6 +349,18 @@ async def _pre_run_hooks(self):
self._hardware.home_z()


def extract_metadata(parsed):
metadata = {}
assigns = [
obj for obj in parsed.body if isinstance(obj, ast.Assign)]
for obj in assigns:
if obj.targets[0].id == 'metadata' and isinstance(obj.value, ast.Dict):
keys = [k.s for k in obj.value.keys]
values = [v.s for v in obj.value.values]
metadata = dict(zip(keys, values))
return metadata


def _accumulate(iterable):
return reduce(
lambda x, y: tuple([x + y for x, y in zip(x, y)]),
Expand Down
2 changes: 1 addition & 1 deletion api/src/opentrons/protocol_api/contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,7 @@ class TemperatureModuleContext(ModuleContext):
def __init__(self, ctx: ProtocolContext,
hw_module: modules.tempdeck.TempDeck,
geometry: ModuleGeometry,
loop: asyncio.AbstractEventLoop):
loop: asyncio.AbstractEventLoop) -> None:
self._module = hw_module
self._loop = loop
super().__init__(ctx, geometry)
Expand Down
49 changes: 48 additions & 1 deletion api/tests/opentrons/api/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

from opentrons.broker import publish
from opentrons.api import Session
from opentrons.api.session import _accumulate, _get_labware, _dedupe
from opentrons.api.session import (
_accumulate, _get_labware, _dedupe, extract_metadata)
from tests.opentrons.conftest import state
from opentrons.legacy_api.robot.robot import Robot
from functools import partial
Expand Down Expand Up @@ -315,3 +316,49 @@ async def test_session_create_error(main_router):
with pytest.raises(TimeoutError):
# No state change is expected
await main_router.wait_until(lambda _: True)


def test_extract_metadata():
import ast

expected = {
'hello': 'world',
'what?': 'no'
}

prot = """
this = 0
that = 1
metadata = {
'what?': 'no',
'hello': 'world'
}
print('wat?')
"""

parsed = ast.parse(prot, filename='testy', mode='exec')
metadata = extract_metadata(parsed)
assert metadata == expected


@pytest.mark.api1_only
async def test_session_metadata(main_router):
expected = {
'hello': 'world',
'what?': 'no'
}

prot = """
this = 0
that = 1
metadata = {
'what?': 'no',
'hello': 'world'
}
print('wat?')
"""

session = await main_router.session_manager.create(
name='<blank>',
text=prot)
assert session.metadata == expected
7 changes: 7 additions & 0 deletions api/tests/opentrons/data/Everything_Test_Software.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
3. Tests different constructor set-ups
"""

metadata = {
'protocolName': 'Everything Test',
'author': 'Opentrons <[email protected]>',
'description': 'A protocol for exercising the API',
'source': 'Opentrons Repository'
}

# Labware Set-up
# Test whether slot naming conventions hold true
tiprack = labware.load('tiprack-200ul', '1')
Expand Down
7 changes: 7 additions & 0 deletions api/tests/opentrons/data/bradford_assay.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
from opentrons import containers, instruments

metadata = {
'protocolName': 'Bradford Assay',
'author': 'Opentrons <[email protected]>',
'description': 'A protien quantification assay',
'source': 'Opentrons Repository'
}

tiprack = containers.load('tiprack-200ul', '9')
tiprack2 = containers.load('tiprack-200ul', '11')

Expand Down
7 changes: 7 additions & 0 deletions api/tests/opentrons/data/calibration-validation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# validate labware calibration instrument selection
from opentrons import containers, instruments

metadata = {
'protocolName': 'Calibration Validation',
'author': 'Opentrons <[email protected]>',
'description': 'For validating the accuracy of a pipette',
'source': 'Opentrons Repository'
}

tiprack_s1 = containers.load('tiprack-200ul', '6', label='s1')
tiprack_s2 = containers.load('tiprack-200ul', '3', label='s2')

Expand Down
6 changes: 6 additions & 0 deletions api/tests/opentrons/data/dinosaur.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from opentrons import containers, instruments

metadata = {
'protocolName': 'Dinosaur',
'author': 'Opentrons <[email protected]>',
'description': 'Simple protocol to draw a dinosaur in a 96 well plate',
'source': 'Opentrons Repository'
}

# a 12 row trough for sources, and 96 well plate for output
trough = containers.load('trough-12row', '3', 'trough')
Expand Down
7 changes: 7 additions & 0 deletions api/tests/opentrons/data/multi-only.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
from opentrons import containers, instruments

metadata = {
'protocolName': 'Multi-only',
'author': 'Opentrons <[email protected]>',
'description': 'A protocol that only uses an 8-channel pipette',
'source': 'Opentrons Repository'
}

tiprack = containers.load('tiprack-200ul', '8')
trough = containers.load('trough-12row', '9')

Expand Down
7 changes: 7 additions & 0 deletions api/tests/opentrons/data/multi-single.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
from opentrons import containers, instruments

metadata = {
'protocolName': 'Multi/Single',
'author': 'Opentrons <[email protected]>',
'description': 'A protocol that uses both 8- and 1-channel pipettes',
'source': 'Opentrons Repository'
}

# from opentrons import robot
# robot.connect()
# robot.home()
Expand Down
6 changes: 6 additions & 0 deletions api/tests/opentrons/data/no_clear_tips.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from opentrons import containers, instruments

metadata = {
'protocolName': 'No-clear-tips',
'author': 'Opentrons <[email protected]>',
'description': 'A test protocol that does not drop tips at the end',
'source': 'Opentrons Repository'
}

# a 12 row trough for sources, and 96 well plate for output
trough = containers.load('trough-12row', '3', 'trough')
Expand Down
7 changes: 7 additions & 0 deletions api/tests/opentrons/data/pickup-accuracy.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
from opentrons import containers, instruments

metadata = {
'protocolName': 'Pickup accuracy',
'author': 'Opentrons <[email protected]>',
'description': 'A protocol for testing pick up and drop tip',
'source': 'Opentrons Repository'
}

# from opentrons import robot
# robot.connect()
# robot.home()
Expand Down
7 changes: 7 additions & 0 deletions api/tests/opentrons/data/smoke.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
from opentrons import containers, instruments

metadata = {
'protocolName': 'Smoke',
'author': 'Opentrons <[email protected]>',
'description': 'Simple protocol to test the server',
'source': 'Opentrons Repository'
}

tiprack = containers.load('tiprack-200ul', '8')
plate = containers.load('96-PCR-flat', '5')

Expand Down
7 changes: 7 additions & 0 deletions api/tests/opentrons/data/testosaur.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
from opentrons import containers, instruments, robot

metadata = {
'protocolName': 'Testosaur',
'author': 'Opentrons <[email protected]>',
'description': 'A variant on "Dinosaur" for testing',
'source': 'Opentrons Repository'
}

p200rack = containers.load('tiprack-200ul', '5', 'tiprack')

# create a p200 pipette on robot axis B
Expand Down

0 comments on commit 1da19bb

Please sign in to comment.