1
0
mirror of https://github.com/gsi-upm/senpy synced 2024-11-21 15:52:28 +00:00

Improve extra requirement handling

This commit adds a new mechanism to handle parameters beforehand in chained
calls, and the ability to get help on available parameters in chained
calls (through `?help`).
It also includes tests for this feature.

Closes #51
This commit is contained in:
J. Fernando Sánchez 2018-08-20 15:44:54 +02:00
parent aa35e62a27
commit ca69bddc17
5 changed files with 326 additions and 21 deletions

View File

@ -175,23 +175,117 @@ def parse_params(indict, *specs):
parameters=outdict, parameters=outdict,
errors=wrong_params) errors=wrong_params)
raise message raise message
if 'algorithm' in outdict and not isinstance(outdict['algorithm'], list):
outdict['algorithm'] = list(outdict['algorithm'].split(','))
return outdict return outdict
def parse_extra_params(request, plugin=None): def get_all_params(plugins, *specs):
params = request.parameters.copy() '''Return a list of parameters for a given set of specifications and plugins.'''
if plugin: dic = {}
extra_params = parse_params(params, plugin.get('extra_params', {})) for s in specs:
params.update(extra_params) dic.update(s)
dic.update(get_extra_params(plugins))
return dic
def get_extra_params(plugins):
'''Get a list of possible parameters given a list of plugins'''
params = {}
extra_params = {}
for i, plugin in enumerate(plugins):
this_params = plugin.get('extra_params', {})
for k, v in this_params.items():
if k not in extra_params:
extra_params[k] = []
extra_params[k].append(v)
params['{}.{}'.format(plugin.name, k)] = v
params['{}.{}'.format(i, k)] = v
for k, v in extra_params.items(): # Resolve conflicts
if len(v) == 1: # Add the extra options that do not collide
params[k] = v[0]
else:
required = False
aliases = None
options = None
default = None
nodefault = False # Set when defaults are not compatible
for opt in v:
required = required or opt.get('required', False)
newaliases = set(opt.get('aliases', []))
if aliases is None:
aliases = newaliases
else:
aliases = aliases & newaliases
if 'options' in opt:
newoptions = set(opt['options'])
options = newoptions if options is None else options & newoptions
if 'default' in opt:
newdefault = opt['default']
if newdefault:
if default is None and not nodefault:
default = newdefault
elif newdefault != default:
nodefault = True
default = None
# Check for incompatibilities
if options != set():
params[k] = {
'default': default,
'aliases': list(aliases),
'required': required,
'options': list(options)
}
return params return params
def parse_extra_params(params, plugins):
'''
Parse the given parameters individually for each plugin, and get a list of the parameters that
belong to each of the plugins. Each item can then be used in the plugin.analyse_entries method.
'''
extra_params = []
for i, plugin in enumerate(plugins):
this_params = filter_params(params, plugin, i)
parsed = parse_params(this_params, plugin.get('extra_params', {}))
extra_params.append(parsed)
return extra_params
def filter_params(params, plugin, ith=-1):
'''
Get the values within params that apply to a plugin.
More specific names override more general names, in this order:
<index_order>.parameter > <plugin.name>.parameter > parameter
Example:
>>> filter_params({'0.hello': True, 'hello': False}, Plugin(), 0)
{ '0.hello': True, 'hello': True}
'''
thisparams = {}
if ith >= 0:
ith = '{}.'.format(ith)
else:
ith = ""
for k, v in params.items():
if ith and k.startswith(str(ith)):
thisparams[k[len(ith):]] = v
elif k.startswith(plugin.name):
thisparams[k[len(plugin.name) + 1:]] = v
elif k not in thisparams:
thisparams[k] = v
return thisparams
def parse_call(params): def parse_call(params):
'''Return a results object based on the parameters used in a call/request. '''Return a results object based on the parameters used in a call/request.
''' '''
params = parse_params(params, NIF_PARAMS) params = parse_params(params, NIF_PARAMS)
if 'algorithm' in params and not isinstance(params['algorithm'], list):
params['algorithm'] = list(params['algorithm'].split(','))
if params['informat'] == 'text': if params['informat'] == 'text':
results = Results() results = Results()
entry = Entry(nif__isString=params['input'], entry = Entry(nif__isString=params['input'],

View File

@ -188,15 +188,20 @@ def basic_api(f):
@api_blueprint.route('/<path:plugin>', methods=['POST', 'GET']) @api_blueprint.route('/<path:plugin>', methods=['POST', 'GET'])
@basic_api @basic_api
def api_root(plugin): def api_root(plugin):
if plugin:
if 'algorithm' in request.parameters:
raise Error('You cannot specify the algorithm with a parameter and a URL variable.'
' Please, remove one of them')
plugin = plugin.replace('+', '/')
request.parameters['algorithm'] = plugin.split('/')
if request.parameters['help']: if request.parameters['help']:
dic = dict(api.API_PARAMS, **api.NIF_PARAMS) sp = current_app.senpy
response = Help(valid_parameters=dic) plugins = sp._get_plugins(request)
allparameters = api.get_all_params(plugins, api.WEB_PARAMS, api.API_PARAMS, api.NIF_PARAMS)
response = Help(valid_parameters=allparameters)
return response return response
req = api.parse_call(request.parameters) req = api.parse_call(request.parameters)
if plugin:
plugin = plugin.replace('+', '/')
plugin = plugin.split('/')
req.parameters['algorithm'] = plugin
return current_app.senpy.analyse(req) return current_app.senpy.analyse(req)

View File

@ -143,7 +143,7 @@ class Senpy(object):
plugins.append(self._plugins[algo]) plugins.append(self._plugins[algo])
return plugins return plugins
def _process_entries(self, entries, req, plugins): def _process_entries(self, entries, req, plugins, extra, ith=0):
""" """
Recursively process the entries with the first plugin in the list, and pass the results Recursively process the entries with the first plugin in the list, and pass the results
to the rest of the plugins. to the rest of the plugins.
@ -153,11 +153,11 @@ class Senpy(object):
yield i yield i
return return
plugin = plugins[0] plugin = plugins[0]
specific_params = api.parse_extra_params(req, plugin) specific_params = extra[ith]
req.analysis.append({'plugin': plugin, req.analysis.append({'plugin': plugin,
'parameters': specific_params}) 'parameters': specific_params})
results = plugin.analyse_entries(entries, specific_params) results = plugin.analyse_entries(entries, specific_params)
for i in self._process_entries(results, req, plugins[1:]): for i in self._process_entries(results, req, plugins[1:], extra, ith=ith + 1):
yield i yield i
def install_deps(self): def install_deps(self):
@ -169,12 +169,16 @@ class Senpy(object):
It takes a processed request, provided by the user, as returned It takes a processed request, provided by the user, as returned
by api.parse_call(). by api.parse_call().
""" """
logger.debug("analysing request: {}".format(request)) logger.debug("analysing request: {}".format(request))
entries = request.entries
request.entries = []
plugins = self._get_plugins(request) plugins = self._get_plugins(request)
extra = api.parse_extra_params(request.parameters, plugins)
entries = request.entries
results = request results = request
for i in self._process_entries(entries, results, plugins): results.entries = []
for i in self._process_entries(entries, results, plugins, extra):
results.entries.append(i) results.entries.append(i)
self.convert_emotions(results) self.convert_emotions(results)
logger.debug("Returning analysis result: {}".format(results)) logger.debug("Returning analysis result: {}".format(results))

View File

@ -3,8 +3,9 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from unittest import TestCase from unittest import TestCase
from senpy.api import parse_params, API_PARAMS, NIF_PARAMS, WEB_PARAMS from senpy.api import (boolean, parse_params, get_extra_params, parse_extra_params,
from senpy.models import Error API_PARAMS, NIF_PARAMS, WEB_PARAMS)
from senpy.models import Error, Plugin
class APITest(TestCase): class APITest(TestCase):
@ -89,3 +90,157 @@ class APITest(TestCase):
assert "Dummy" in p['algorithm'] assert "Dummy" in p['algorithm']
assert 'input' in p assert 'input' in p
assert p['input'] == 'Aloha my friend' assert p['input'] == 'Aloha my friend'
def test_parse_extra_params(self):
'''The API should parse user parameters and return them in a format that plugins can use'''
plugins = [
Plugin({
'name': 'plugin1',
'extra_params': {
# Incompatible parameter
'param0': {
'aliases': ['p1', 'parameter1'],
'options': ['option1', 'option2'],
'default': 'option1',
'required': True
},
'param1': {
'aliases': ['p1', 'parameter1'],
'options': ['en', 'es'],
'default': 'en',
'required': False
},
'param2': {
'aliases': ['p2', 'parameter2'],
'required': False,
'options': ['value2_1', 'value2_2', 'value3_3']
}
}
}), Plugin({
'name': 'plugin2',
'extra_params': {
'param0': {
'aliases': ['parameter1'],
'options': ['new option', 'new option2'],
'default': 'new option',
'required': False
},
'param1': {
'aliases': ['myparam1', 'p1'],
'options': ['en', 'de', 'auto'],
'default': 'de',
'required': True
},
'param3': {
'aliases': ['p3', 'parameter3'],
'options': boolean,
'default': True
}
}
})
]
call = {
'param1': 'en',
'0.param0': 'option1',
'0.param1': 'en',
'param2': 'value2_1',
'param0': 'new option',
'1.param1': 'de',
'param3': False,
}
expected = [
{
'param0': 'option1',
'param1': 'en',
'param2': 'value2_1',
}, {
'param0': 'new option',
'param1': 'de',
'param3': False,
}
]
p = parse_extra_params(call, plugins)
for i, arg in enumerate(expected):
for k, v in arg.items():
assert p[i][k] == v
def test_get_extra_params(self):
'''The API should return the list of valid parameters for a set of plugins'''
plugins = [
Plugin({
'name': 'plugin1',
'extra_params': {
# Incompatible parameter
'param0': {
'aliases': ['p1', 'parameter1'],
'options': ['option1', 'option2'],
'default': 'option1',
'required': True
},
'param1': {
'aliases': ['p1', 'parameter1'],
'options': ['en', 'es'],
'default': 'en',
'required': False
},
'param2': {
'aliases': ['p2', 'parameter2'],
'required': False,
'options': ['value2_1', 'value2_2', 'value3_3']
}
}
}), Plugin({
'name': 'plugin2',
'extra_params': {
'param0': {
'aliases': ['parameter1'],
'options': ['new option', 'new option2'],
'default': 'new option',
'required': False
},
'param1': {
'aliases': ['myparam1', 'p1'],
'options': ['en', 'de', 'auto'],
'default': 'de',
'required': True
},
'param3': {
'aliases': ['p3', 'parameter3'],
'options': boolean,
'default': True
}
}
})
]
expected = {
# Each plugin's parameters
'0.param0': plugins[0]['extra_params']['param0'],
'0.param1': plugins[0]['extra_params']['param1'],
'0.param2': plugins[0]['extra_params']['param2'],
'1.param0': plugins[1]['extra_params']['param0'],
'1.param1': plugins[1]['extra_params']['param1'],
'1.param3': plugins[1]['extra_params']['param3'],
# Non-overlapping parameters
'param2': plugins[0]['extra_params']['param2'],
'param3': plugins[1]['extra_params']['param3'],
# Intersection of overlapping parameters
'param1': {
'aliases': ['p1'],
'options': ['en'],
'default': None,
'required': True
}
}
result = get_extra_params(plugins)
for ik, iv in expected.items():
assert ik in result
for jk, jv in iv.items():
assert jk in result[ik]
assert expected[ik][jk] == result[ik][jk]

View File

@ -133,6 +133,53 @@ class BlueprintsTest(TestCase):
assert len(js['analysis']) == 2 assert len(js['analysis']) == 2
assert js['entries'][0]['nif:isString'] == 'My aloha mohame' assert js['entries'][0]['nif:isString'] == 'My aloha mohame'
def test_requirements_chain_help(self):
'''The extra parameters of each plugin should be merged if they are in a chain '''
resp = self.client.get("/api/split/DummyRequired?help=true")
self.assertCode(resp, 200)
js = parse_resp(resp)
assert 'valid_parameters' in js
vp = js['valid_parameters']
assert 'example' in vp
def test_requirements_chain_repeat_help(self):
'''
If a plugin appears several times in a chain, there should be a way to set different
parameters for each.
'''
resp = self.client.get("/api/split/split?help=true")
self.assertCode(resp, 200)
js = parse_resp(resp)
assert 'valid_parameters' in js
vp = js['valid_parameters']
assert '0.delimiter' in vp
assert '1.delimiter' in vp
assert 'delimiter' in vp
def test_requirements_chain(self):
"""
It should be possible to specify different parameters for each step in the chain.
"""
# First, we split by sentence twice. Each call should generate 3 additional entries
# (one per sentence in the original).
resp = self.client.get('/api/split/split?i=The first sentence. The second sentence.'
'\nA new paragraph&delimiter=sentence')
js = parse_resp(resp)
assert len(js['analysis']) == 2
assert len(js['entries']) == 7
# Now, we split by sentence. This produces 3 additional entries.
# Then, we split by paragraph. This should create 2 additional entries (One per paragraph
# in the original text)
resp = self.client.get('/api/split/split?i=The first sentence. The second sentence.'
'\nA new paragraph&0.delimiter=sentence&1.delimiter=paragraph')
# Calling dummy twice, should return the same string
self.assertCode(resp, 200)
js = parse_resp(resp)
assert len(js['analysis']) == 2
assert len(js['entries']) == 6
def test_error(self): def test_error(self):
""" """
The dummy plugin returns an empty response,\ The dummy plugin returns an empty response,\