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

feat(api): Add advanced settings endpoints to api server #1786

Merged
merged 1 commit into from
Jul 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
142 changes: 142 additions & 0 deletions api/opentrons/config/advanced_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
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()
return s.get(_id).get('value')


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')
39 changes: 38 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,38 @@ 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 = {'message': 'ID {} not found in 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