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 Jun 29, 2018
1 parent 11f582b commit 7f094e1
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 102 deletions.
127 changes: 127 additions & 0 deletions api/opentrons/config/advanced_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
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 bool(s.get(_id))


def get_all_adv_settings() -> dict:
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 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))
64 changes: 11 additions & 53 deletions api/opentrons/config/feature_flags.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,21 @@
import os
import json
from opentrons.config import get_config_index
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
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
36 changes: 36 additions & 0 deletions api/tests/opentrons/server/test_settings_endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from opentrons.server.main import init


def validate_response_body(body):
settings_list = body.get('settings')
assert type(settings_list) == list
for obj in settings_list:
assert 'id' in obj, '"id" field not found in settings object'
assert 'title' in obj, '"title" not found for {}'.format(obj['id'])
assert 'description' in obj, '"description" not found for {}'.format(
obj['id'])
assert 'value' in obj, '"value" not found for {}'.format(obj['id'])


async def test_get(virtual_smoothie_env, loop, test_client):
app = init(loop)
cli = await loop.create_task(test_client(app))

resp = await cli.get('/settings')
body = await resp.json()
assert resp.status == 200
validate_response_body(body)


async def test_set(virtual_smoothie_env, loop, test_client):
app = init(loop)
cli = await loop.create_task(test_client(app))
test_id = 'disableHomeOnBoot'

resp = await cli.post('/settings', json={"id": test_id, "value": True})
body = await resp.json()
assert resp.status == 200
validate_response_body(body)
test_setting = list(
filter(lambda x: x.get('id') == test_id, body.get('settings')))[0]
assert test_setting.get('value')

0 comments on commit 7f094e1

Please sign in to comment.