diff --git a/api/docs/source/index.rst b/api/docs/source/index.rst index 0b1d46b8807..4d3dccab902 100644 --- a/api/docs/source/index.rst +++ b/api/docs/source/index.rst @@ -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 ', + '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]) @@ -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 ^^^^^^^ @@ -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 ^^^^^^^ diff --git a/api/src/opentrons/api/session.py b/api/src/opentrons/api/session.py index d4394e8e7f0..d6676d98421 100755 --- a/api/src/opentrons/api/session.py +++ b/api/src/opentrons/api/session.py @@ -80,6 +80,7 @@ def __init__(self, name, text, hardware): self.instruments = None self.containers = None self.modules = None + self.metadata = {} self.startTime = None @@ -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) @@ -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)]), diff --git a/api/src/opentrons/protocol_api/contexts.py b/api/src/opentrons/protocol_api/contexts.py index 0c9f346fa2f..ba1da992596 100644 --- a/api/src/opentrons/protocol_api/contexts.py +++ b/api/src/opentrons/protocol_api/contexts.py @@ -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) diff --git a/api/tests/opentrons/api/test_session.py b/api/tests/opentrons/api/test_session.py index 368277f87e7..082093c0c70 100755 --- a/api/tests/opentrons/api/test_session.py +++ b/api/tests/opentrons/api/test_session.py @@ -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 @@ -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='', + text=prot) + assert session.metadata == expected diff --git a/api/tests/opentrons/data/Everything_Test_Software.py b/api/tests/opentrons/data/Everything_Test_Software.py index 7290b4042f7..394939054c3 100644 --- a/api/tests/opentrons/data/Everything_Test_Software.py +++ b/api/tests/opentrons/data/Everything_Test_Software.py @@ -8,6 +8,13 @@ 3. Tests different constructor set-ups """ +metadata = { + 'protocolName': 'Everything Test', + 'author': 'Opentrons ', + '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') diff --git a/api/tests/opentrons/data/bradford_assay.py b/api/tests/opentrons/data/bradford_assay.py index 62b503688cd..8b2ad5d0e35 100644 --- a/api/tests/opentrons/data/bradford_assay.py +++ b/api/tests/opentrons/data/bradford_assay.py @@ -1,5 +1,12 @@ from opentrons import containers, instruments +metadata = { + 'protocolName': 'Bradford Assay', + 'author': 'Opentrons ', + 'description': 'A protien quantification assay', + 'source': 'Opentrons Repository' +} + tiprack = containers.load('tiprack-200ul', '9') tiprack2 = containers.load('tiprack-200ul', '11') diff --git a/api/tests/opentrons/data/calibration-validation.py b/api/tests/opentrons/data/calibration-validation.py index e7da539a131..425cbe87bef 100644 --- a/api/tests/opentrons/data/calibration-validation.py +++ b/api/tests/opentrons/data/calibration-validation.py @@ -1,6 +1,13 @@ # validate labware calibration instrument selection from opentrons import containers, instruments +metadata = { + 'protocolName': 'Calibration Validation', + 'author': 'Opentrons ', + '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') diff --git a/api/tests/opentrons/data/dinosaur.py b/api/tests/opentrons/data/dinosaur.py index 3132a4f2484..6fc9cc0785c 100644 --- a/api/tests/opentrons/data/dinosaur.py +++ b/api/tests/opentrons/data/dinosaur.py @@ -1,5 +1,11 @@ from opentrons import containers, instruments +metadata = { + 'protocolName': 'Dinosaur', + 'author': 'Opentrons ', + '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') diff --git a/api/tests/opentrons/data/multi-only.py b/api/tests/opentrons/data/multi-only.py index e71697d794d..eeefa913311 100644 --- a/api/tests/opentrons/data/multi-only.py +++ b/api/tests/opentrons/data/multi-only.py @@ -1,5 +1,12 @@ from opentrons import containers, instruments +metadata = { + 'protocolName': 'Multi-only', + 'author': 'Opentrons ', + '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') diff --git a/api/tests/opentrons/data/multi-single.py b/api/tests/opentrons/data/multi-single.py index 473d8603121..ec9b306c93e 100644 --- a/api/tests/opentrons/data/multi-single.py +++ b/api/tests/opentrons/data/multi-single.py @@ -1,5 +1,12 @@ from opentrons import containers, instruments +metadata = { + 'protocolName': 'Multi/Single', + 'author': 'Opentrons ', + 'description': 'A protocol that uses both 8- and 1-channel pipettes', + 'source': 'Opentrons Repository' +} + # from opentrons import robot # robot.connect() # robot.home() diff --git a/api/tests/opentrons/data/no_clear_tips.py b/api/tests/opentrons/data/no_clear_tips.py index 9eea02d8168..a26b7e1eb93 100644 --- a/api/tests/opentrons/data/no_clear_tips.py +++ b/api/tests/opentrons/data/no_clear_tips.py @@ -1,5 +1,11 @@ from opentrons import containers, instruments +metadata = { + 'protocolName': 'No-clear-tips', + 'author': 'Opentrons ', + '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') diff --git a/api/tests/opentrons/data/pickup-accuracy.py b/api/tests/opentrons/data/pickup-accuracy.py index c2863fc1b46..4801928ba75 100644 --- a/api/tests/opentrons/data/pickup-accuracy.py +++ b/api/tests/opentrons/data/pickup-accuracy.py @@ -1,5 +1,12 @@ from opentrons import containers, instruments +metadata = { + 'protocolName': 'Pickup accuracy', + 'author': 'Opentrons ', + 'description': 'A protocol for testing pick up and drop tip', + 'source': 'Opentrons Repository' +} + # from opentrons import robot # robot.connect() # robot.home() diff --git a/api/tests/opentrons/data/smoke.py b/api/tests/opentrons/data/smoke.py index 5629af236fe..fec88e52cd2 100644 --- a/api/tests/opentrons/data/smoke.py +++ b/api/tests/opentrons/data/smoke.py @@ -1,5 +1,12 @@ from opentrons import containers, instruments +metadata = { + 'protocolName': 'Smoke', + 'author': 'Opentrons ', + 'description': 'Simple protocol to test the server', + 'source': 'Opentrons Repository' +} + tiprack = containers.load('tiprack-200ul', '8') plate = containers.load('96-PCR-flat', '5') diff --git a/api/tests/opentrons/data/testosaur.py b/api/tests/opentrons/data/testosaur.py index 14a9af61113..fee8089189f 100644 --- a/api/tests/opentrons/data/testosaur.py +++ b/api/tests/opentrons/data/testosaur.py @@ -1,5 +1,12 @@ from opentrons import containers, instruments, robot +metadata = { + 'protocolName': 'Testosaur', + 'author': 'Opentrons ', + '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