Skip to content

Commit

Permalink
feat(api): Add advanced settings endpoints to api server
Browse files Browse the repository at this point in the history
Fixes #1656
  • Loading branch information
btmorr committed Jul 2, 2018
1 parent 11f582b commit 865f866
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 133 deletions.
144 changes: 144 additions & 0 deletions api/opentrons/config/advanced_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import json
import logging
from copy import copy
from opentrons.config import get_config_index

log = logging.getLogger(__name__)


class Setting:
def __init__(self, _id, title, description, old_id=None):
self.id = _id
self.old_id = old_id
self.title = title
self.description = description

def __repr__(self):
return '{}: {}'.format(self.__class__, self.id)


settings = [
Setting(
_id='shortFixedTrash',
old_id='short-fixed-trash',
title='Short (55mm) fixed trash',
description='Trash box is 55mm tall (rather than the 77mm default)'
),
Setting(
_id='splitLabwareDefinitions',
old_id='split-labware-def',
title='New JSON labware definitions',
description='JSON labware definitions with a separate def file and'
' offset file for each labware'
),
Setting(
_id='calibrateToBottom',
old_id='calibrate-to-bottom',
title='Calibrate to bottom',
description='Calibrate using the bottom-center of well A1 for each'
' labware (rather than the top-center)'
),
Setting(
_id='deckCalibrationDots',
old_id='dots-deck-type',
title='Deck calibration to dots',
description='Perform deck calibration to dots rather than crosses, for'
' robots that do not have crosses etched on the deck'
),
Setting(
_id='disableHomeOnBoot',
old_id='disable-home-on-boot',
title='Disable home on boot',
description='Prevent robot from homing motors on boot'
)
]

settings_by_id = {s.id: s for s in settings}
settings_by_old_id = {s.old_id: s for s in settings}


def get_adv_setting(_id: str) -> bool:
_id = _clean_id(_id)
s = get_all_adv_settings()
res = s.get(_id).get('value')
print("================================")
return res


def get_all_adv_settings() -> dict:
"""
:return: a dict of settings keyed by setting ID, where each value is a
dict with keys "id", "title", "description", and "value"
"""
settings_file = get_config_index().get('featureFlagFile')

values = _read_settings_file(settings_file)
for key, value in values.items():
s = copy(settings_by_id[key].__dict__)
s.pop('old_id')
values[key] = s
values[key]['value'] = value
return values


def set_adv_setting(_id: str, value):
_id = _clean_id(_id)
settings_file = get_config_index().get('featureFlagFile')
s = _read_settings_file(settings_file)
s[_id] = value
_write_settings_file(s, settings_file)


def _clean_id(_id: str) -> str:
if _id in settings_by_old_id.keys():
_id = settings_by_old_id[_id].id
return _id


def _read_json_file(path: str) -> dict:
try:
with open(path, 'r') as fd:
data = json.load(fd)
except FileNotFoundError:
data = {}
return data


def _read_settings_file(settings_file: str) -> dict:
"""
Read the settings file, which is a json object with settings IDs as keys
and boolean values. For each key, look up the `Settings` object with that
key. If the key is one of the old IDs (kebab case), replace it with the
new ID and rewrite the settings file
:param settings_file: the path to the settings file
:return: a dict with all new settings IDs as the keys, and boolean values
(the values stored in the settings file, or `False` if the key was not
found).
"""
# Read settings from persistent file
data = _read_json_file(settings_file)
all_ids = [s.id for s in settings]

# If any old keys are stored in the file, replace them with the new key
old_keys = settings_by_old_id.keys()
if any([k in old_keys for k in data.keys()]):
for v in data.keys():
if v in old_keys:
new_key = settings_by_old_id.get(v).id
data[new_key] = data[v]
data.pop(v)
_write_settings_file(data, settings_file)

# If any settings do not have a key in the data, default to `False`
res = {key: data.get(key, False) for key in all_ids}
return res


def _write_settings_file(data: dict, settings_file: str):
try:
with open(settings_file, 'w') as fd:
json.dump(data, fd)
except OSError:
log.exception('Failed to write advanced settings file to: {}'.format(
settings_file))
66 changes: 13 additions & 53 deletions api/opentrons/config/feature_flags.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,23 @@
import os
import json
from opentrons.config import get_config_index
from functools import lru_cache
from opentrons.config import advanced_settings as advs


def get_feature_flag(name: str) -> bool:
settings = get_all_feature_flags()
return bool(settings.get(name))
def short_fixed_trash():
return advs.get_adv_setting('shortFixedTrash')


def get_all_feature_flags() -> dict:
settings_file = get_config_index().get('featureFlagFile')
if settings_file and os.path.exists(settings_file):
with open(settings_file, 'r') as fd:
settings = json.load(fd)
else:
settings = {}
return settings
@lru_cache()
def split_labware_definitions():
return advs.get_adv_setting('splitLabwareDefinitions')


def set_feature_flag(name: str, value):
settings_file = get_config_index().get('featureFlagFile')
if os.path.exists(settings_file):
with open(settings_file, 'r') as fd:
settings = json.load(fd)
settings[name] = value
else:
settings = {name: value}
with open(settings_file, 'w') as fd:
json.dump(settings, fd)
def calibrate_to_bottom():
return advs.get_adv_setting('calibrateToBottom')


# short_fixed_trash
# - True ('55.0'): Old (55mm tall) fixed trash
# - False: 77mm tall fixed trash
# - EOL: when all short fixed trash containers have been replaced
def short_fixed_trash(): return get_feature_flag('short-fixed-trash')
def dots_deck_type():
return advs.get_adv_setting('deckCalibrationDots')


# split_labware_definitions
# - True: Use new labware definitions (See: labware_definitions.py and
# serializers.py)
# - False: Use sqlite db
def split_labware_definitions(): return get_feature_flag('split-labware-def')


# calibrate_to_bottom
# - True: You must calibrate your containers to bottom
# - False: Otherwise the default
# will be that you calibrate to the top
def calibrate_to_bottom(): return get_feature_flag('calibrate-to-bottom')


# dots_deck_type
# - True: The deck layout has etched "dots"
# - False: The deck layout has etched "crosses"
def dots_deck_type(): return get_feature_flag('dots-deck-type')


# disable_home_on_boot
# - True: The robot should not home the carriages on boot
# - False: The robot should home the carriages on boot
def disable_home_on_boot(): return get_feature_flag('disable-home-on-boot')
def disable_home_on_boot():
return advs.get_adv_setting('disableHomeOnBoot')
41 changes: 40 additions & 1 deletion api/opentrons/server/endpoints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import logging
from aiohttp import web
from opentrons import robot, __version__
from opentrons.config import advanced_settings as advs


log = logging.getLogger(__name__)

Expand All @@ -12,7 +14,7 @@
os.environ.get('RESIN_DEVICE_NAME_AT_INIT', 'dev'))


async def health(request):
async def health(request: web.Request) -> web.Response:
res = {
'name': NAME,
'api_version': __version__,
Expand All @@ -21,3 +23,40 @@ async def health(request):
return web.json_response(
headers={'Access-Control-Allow-Origin': '*'},
body=json.dumps(res))


async def get_advanced_settings(request: web.Request) -> web.Response:
"""
Handles a GET request and returns a json body with the key "settings" and a
value that is a list of objects where each object has keys "id", "title",
"description", and "value"
"""
res = _get_adv_settings()
return web.json_response(res)


def _get_adv_settings() -> dict:
data = advs.get_all_adv_settings()
return {"settings": list(data.values())}


async def set_advanced_setting(request: web.Request) -> web.Response:
"""
Handles a POST request with a json body that has keys "id" and "value",
where the value of "id" must correspond to an id field of a setting in
`opentrons.config.advanced_settings.settings`. Saves the value of "value"
for the setting that matches the supplied id.
"""
data = await request.json()
key = data.get('id')
value = data.get('value')
if key and key in advs.settings_by_id.keys():
advs.set_adv_setting(key, value)
res = _get_adv_settings()
status = 200
else:
res = _get_adv_settings()
res.update(
{'error': 'ID {} not found in advanced settings list'.format(key)})
status = 400
return web.json_response(res, status=status)
3 changes: 2 additions & 1 deletion api/opentrons/server/endpoints/serverlib_fallback.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# This file duplicates the implementation of ot2serverlib
# This file duplicates the implementation of ot2serverlib. Remove once all
# robots have new update endpoints
import os
import json
import asyncio
Expand Down
42 changes: 0 additions & 42 deletions api/opentrons/server/endpoints/update.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import os
import logging
import asyncio
from aiohttp import web
from opentrons.config import feature_flags as ff
from opentrons import robot

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -45,42 +42,3 @@ async def _update_firmware(filename, loop):
robot._driver._setup()

return res


async def set_feature_flag(request):
"""
Post body must include the keys 'key' and 'value'. The values of these two
entries will be set in the settings file as a key-value pair.
"""
try:
data = await request.json()
flag_name = data.get('key')
flag_value = data.get('value')
log.debug("Set feature flag '{}' to '{}' (prior value: '{}')".format(
flag_name, flag_value, ff.get_feature_flag(flag_name)))

ff.set_feature_flag(flag_name, flag_value)

message = "Set '{}' to '{}'".format(flag_name, flag_value)
status = 200
except Exception as e:
message = 'Error: {}'.format(e)
status = 400
return web.json_response({'message': message}, status=status)


async def get_feature_flag(request):
"""
URI path should specify the {flag} match parameter. The 'all' flag is
reserved to return the dict of all managed settings.
"""
res = ff.get_all_feature_flags()
return web.json_response(res)


async def environment(request):
res = dict(os.environ)
api_keys = filter(lambda x: "KEY" in x, list(res.keys()))
for key in api_keys:
res.pop(key)
return web.json_response(res)
8 changes: 3 additions & 5 deletions api/opentrons/server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from opentrons.api import MainRouter
from opentrons.server.rpc import Server
from opentrons.server import endpoints as endp
from opentrons.server.endpoints import (wifi, control, update)
from opentrons.server.endpoints import (wifi, control)
from opentrons.config import feature_flags as ff
from opentrons.util import environment
from opentrons.deck_calibration import endpoints as dc_endp
Expand Down Expand Up @@ -185,11 +185,9 @@ def init(loop=None):
server.app.router.add_post(
'/robot/lights', control.set_rail_lights)
server.app.router.add_get(
'/settings', update.get_feature_flag)
server.app.router.add_get(
'/settings/environment', update.environment)
'/settings', endp.get_advanced_settings)
server.app.router.add_post(
'/settings/set', update.set_feature_flag)
'/settings', endp.set_advanced_setting)

return server.app

Expand Down
6 changes: 3 additions & 3 deletions api/tests/opentrons/cli/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest
from opentrons.config import get_config_index
from opentrons.config import feature_flags as ff
from opentrons.config import advanced_settings as advs


@pytest.fixture
Expand Down Expand Up @@ -55,7 +55,7 @@ async def test_new_deck_points():
# if feature_flag is set (or not)
from opentrons.deck_calibration.dc_main import get_calibration_points
from opentrons.deck_calibration.endpoints import expected_points
ff.set_feature_flag('dots-deck-type', True)
advs.set_adv_setting('deckCalibrationDots', True)
calibration_points = get_calibration_points()
expected_points1 = expected_points()
# Check that old calibration points are used in cli
Expand All @@ -67,7 +67,7 @@ async def test_new_deck_points():
assert expected_points1['2'] == (380.87, 6.0)
assert expected_points1['3'] == (12.13, 261.0)

ff.set_feature_flag('dots-deck-type', False)
advs.set_adv_setting('deckCalibrationDots', False)
calibration_points2 = get_calibration_points()
expected_points2 = expected_points()
# Check that new calibration points are used
Expand Down
Loading

0 comments on commit 865f866

Please sign in to comment.