1
0
mirror of https://github.com/gsi-upm/senpy synced 2024-11-22 08:12:27 +00:00

Several fixes

* Refactored BaseModel for efficiency
* Added plugin metaclass to keep track of plugin types
* Moved plugins to examples dir (in a previous commit)
* Simplified validation in parse_params
* Added convenience methods to mock requests in tests
* Changed help schema to use `.valid_parameters` instead of `.parameters`,
which was used in results to show parameters provided by the user.
* Improved UI
    * Added basic parameters
    * Fixed bugs in parameter handling
    * Refactored and cleaned code
This commit is contained in:
J. Fernando Sánchez 2018-01-01 13:13:17 +01:00
parent f93eed2cf5
commit bfc588a915
35 changed files with 845 additions and 445 deletions

View File

@ -0,0 +1,26 @@
from senpy.plugins import AnalysisPlugin
import multiprocessing
def _train(process_number):
return process_number
class AsyncPlugin(AnalysisPlugin):
def _do_async(self, num_processes):
pool = multiprocessing.Pool(processes=num_processes)
values = pool.map(_train, range(num_processes))
return values
def activate(self):
self.value = self._do_async(4)
def analyse_entry(self, entry, params):
values = self._do_async(2)
entry.async_values = values
yield entry
def test(self):
pass

View File

@ -0,0 +1,8 @@
---
name: Async
module: asyncplugin
description: I am async
author: "@balkian"
version: '0.1'
async: true
extra_params: {}

View File

@ -0,0 +1,11 @@
from senpy.plugins import SentimentPlugin
class DummyPlugin(SentimentPlugin):
def analyse_entry(self, entry, params):
entry['nif:isString'] = entry['nif:isString'][::-1]
entry.reversed = entry.get('reversed', 0) + 1
yield entry
def test(self):
pass

View File

@ -0,0 +1,15 @@
{
"name": "Dummy",
"module": "dummy",
"description": "I am dummy",
"author": "@balkian",
"version": "0.1",
"extra_params": {
"example": {
"@id": "example_parameter",
"aliases": ["example", "ex"],
"required": false,
"default": 0
}
}
}

View File

@ -0,0 +1,27 @@
from senpy.plugins import SentimentPlugin
class DummyPlugin(SentimentPlugin):
description = 'This is a dummy self-contained plugin'
author = '@balkian'
version = '0.1'
def analyse_entry(self, entry, params):
entry['nif:isString'] = entry['nif:isString'][::-1]
entry.reversed = entry.get('reversed', 0) + 1
yield entry
test_cases = [{
"entry": {
"nif:isString": "Hello world!"
},
"expected": [{
"nif:isString": "!dlrow olleH"
}]
}]
if __name__ == '__main__':
d = DummyPlugin()
d.test()

View File

@ -0,0 +1,2 @@
name: DummyNoInfo
module: dummy_noinfo

View File

@ -0,0 +1,14 @@
{
"name": "DummyRequired",
"module": "dummy",
"description": "I am dummy",
"author": "@balkian",
"version": "0.1",
"extra_params": {
"example": {
"@id": "example_parameter",
"aliases": ["example", "ex"],
"required": true
}
}
}

View File

@ -0,0 +1,5 @@
from senpy.plugins import SentimentPlugin
class DummyPlugin(SentimentPlugin):
import noop

View File

@ -0,0 +1,14 @@
from senpy.plugins import AnalysisPlugin
from time import sleep
class SleepPlugin(AnalysisPlugin):
def activate(self, *args, **kwargs):
sleep(self.timeout)
def analyse_entry(self, entry, params):
sleep(float(params.get("timeout", self.timeout)))
yield entry
def test(self):
pass

View File

@ -0,0 +1,16 @@
{
"name": "Sleep",
"module": "sleep",
"description": "I am dummy",
"author": "@balkian",
"version": "0.1",
"timeout": 0.05,
"extra_params": {
"timeout": {
"@id": "timeout_sleep",
"aliases": ["timeout", "to"],
"required": false,
"default": 0
}
}
}

View File

@ -3,7 +3,6 @@ requests>=2.4.1
tornado>=4.4.3 tornado>=4.4.3
PyLD>=0.6.5 PyLD>=0.6.5
nltk nltk
six
future future
jsonschema jsonschema
jsonref jsonref

View File

@ -19,6 +19,9 @@ Sentiment analysis server in Python
""" """
from .version import __version__ from .version import __version__
from future.standard_library import install_aliases
install_aliases()
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -67,7 +67,7 @@ def main():
'--plugins-folder', '--plugins-folder',
'-f', '-f',
type=str, type=str,
default='plugins', default='.',
help='Where to look for plugins.') help='Where to look for plugins.')
parser.add_argument( parser.add_argument(
'--only-install', '--only-install',

View File

@ -13,8 +13,9 @@ API_PARAMS = {
"expanded-jsonld": { "expanded-jsonld": {
"@id": "expanded-jsonld", "@id": "expanded-jsonld",
"aliases": ["expanded"], "aliases": ["expanded"],
"options": "boolean",
"required": True, "required": True,
"default": 0 "default": False
}, },
"with_parameters": { "with_parameters": {
"aliases": ['withparameters', "aliases": ['withparameters',
@ -23,13 +24,6 @@ API_PARAMS = {
"default": False, "default": False,
"required": True "required": True
}, },
"plugin_type": {
"@id": "pluginType",
"description": 'What kind of plugins to list',
"aliases": ["pluginType"],
"required": True,
"default": "analysisPlugin"
},
"outformat": { "outformat": {
"@id": "outformat", "@id": "outformat",
"aliases": ["o"], "aliases": ["o"],
@ -59,6 +53,16 @@ API_PARAMS = {
} }
} }
PLUGINS_PARAMS = {
"plugin_type": {
"@id": "pluginType",
"description": 'What kind of plugins to list',
"aliases": ["pluginType"],
"required": True,
"default": 'analysisPlugin'
}
}
WEB_PARAMS = { WEB_PARAMS = {
"inHeaders": { "inHeaders": {
"aliases": ["headers"], "aliases": ["headers"],
@ -126,7 +130,8 @@ def parse_params(indict, *specs):
wrong_params = {} wrong_params = {}
for spec in specs: for spec in specs:
for param, options in iteritems(spec): for param, options in iteritems(spec):
if param[0] != "@": # Exclude json-ld properties if param[0] == "@": # Exclude json-ld properties
continue
for alias in options.get("aliases", []): for alias in options.get("aliases", []):
# Replace each alias with the correct name of the parameter # Replace each alias with the correct name of the parameter
if alias in indict and alias is not param: if alias in indict and alias is not param:
@ -134,15 +139,16 @@ def parse_params(indict, *specs):
del indict[alias] del indict[alias]
continue continue
if param not in outdict: if param not in outdict:
if options.get("required", False) and "default" not in options:
wrong_params[param] = spec[param]
else:
if "default" in options: if "default" in options:
# We assume the default is correct
outdict[param] = options["default"] outdict[param] = options["default"]
elif "options" in spec[param]: elif options.get("required", False):
if spec[param]["options"] == "boolean": wrong_params[param] = spec[param]
continue
if "options" in options:
if options["options"] == "boolean":
outdict[param] = outdict[param] in [None, True, 'true', '1'] outdict[param] = outdict[param] in [None, True, 'true', '1']
elif outdict[param] not in spec[param]["options"]: elif outdict[param] not in options["options"]:
wrong_params[param] = spec[param] wrong_params[param] = spec[param]
if wrong_params: if wrong_params:
logger.debug("Error parsing: %s", wrong_params) logger.debug("Error parsing: %s", wrong_params)
@ -158,7 +164,7 @@ def parse_params(indict, *specs):
return outdict return outdict
def get_extra_params(request, plugin=None): def parse_extra_params(request, plugin=None):
params = request.parameters.copy() params = request.parameters.copy()
if plugin: if plugin:
extra_params = parse_params(params, plugin.get('extra_params', {})) extra_params = parse_params(params, plugin.get('extra_params', {}))
@ -177,6 +183,6 @@ def parse_call(params):
elif params['informat'] == 'json-ld': elif params['informat'] == 'json-ld':
results = from_string(params['input'], cls=Results) results = from_string(params['input'], cls=Results)
else: else:
raise NotImplemented('Informat {} is not implemented'.format(params['informat'])) raise NotImplementedError('Informat {} is not implemented'.format(params['informat']))
results.parameters = params results.parameters = params
return results return results

View File

@ -25,6 +25,7 @@ from .version import __version__
from functools import wraps from functools import wraps
import logging import logging
import traceback
import json import json
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -72,12 +73,19 @@ def schema(schema="definitions"):
def basic_api(f): def basic_api(f):
default_params = {
'inHeaders': False,
'expanded-jsonld': False,
'outformat': 'json-ld',
'with_parameters': True,
}
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
raw_params = get_params(request) raw_params = get_params(request)
headers = {'X-ORIGINAL-PARAMS': json.dumps(raw_params)} headers = {'X-ORIGINAL-PARAMS': json.dumps(raw_params)}
params = default_params
outformat = 'json-ld'
try: try:
print('Getting request:') print('Getting request:')
print(request) print(request)
@ -87,26 +95,32 @@ def basic_api(f):
else: else:
request.parameters = params request.parameters = params
response = f(*args, **kwargs) response = f(*args, **kwargs)
except Error as ex: except (Exception) as ex:
response = ex
response.parameters = params
logger.error(ex)
if current_app.debug: if current_app.debug:
raise raise
if not isinstance(ex, Error):
msg = "{}:\n\t{}".format(ex,
traceback.format_exc())
ex = Error(message=msg, status=500)
logger.exception('Error returning analysis result')
response = ex
response.parameters = raw_params
logger.error(ex)
in_headers = params['inHeaders'] if 'parameters' in response and not params['with_parameters']:
expanded = params['expanded-jsonld'] print(response)
outformat = params['outformat'] print(response.data)
del response.parameters
return response.flask( return response.flask(
in_headers=in_headers, in_headers=params['inHeaders'],
headers=headers, headers=headers,
prefix=url_for('.api_root', _external=True), prefix=url_for('.api_root', _external=True),
context_uri=url_for('api.context', context_uri=url_for('api.context',
entity=type(response).__name__, entity=type(response).__name__,
_external=True), _external=True),
outformat=outformat, outformat=params['outformat'],
expanded=expanded) expanded=params['expanded-jsonld'])
return decorated_function return decorated_function
@ -116,19 +130,18 @@ def basic_api(f):
def api_root(): def api_root():
if request.parameters['help']: if request.parameters['help']:
dic = dict(api.API_PARAMS, **api.NIF_PARAMS) dic = dict(api.API_PARAMS, **api.NIF_PARAMS)
response = Help(parameters=dic) response = Help(valid_parameters=dic)
return response return response
else:
req = api.parse_call(request.parameters) req = api.parse_call(request.parameters)
response = current_app.senpy.analyse(req) return current_app.senpy.analyse(req)
return response
@api_blueprint.route('/plugins/', methods=['POST', 'GET']) @api_blueprint.route('/plugins/', methods=['POST', 'GET'])
@basic_api @basic_api
def plugins(): def plugins():
sp = current_app.senpy sp = current_app.senpy
ptype = request.parameters.get('plugin_type') params = api.parse_params(request.parameters, api.PLUGINS_PARAMS)
ptype = params.get('plugin_type')
plugins = sp.filter_plugins(plugin_type=ptype) plugins = sp.filter_plugins(plugin_type=ptype)
dic = Plugins(plugins=list(plugins.values())) dic = Plugins(plugins=list(plugins.values()))
return dic return dic

View File

@ -1,7 +1,6 @@
import requests import requests
import logging import logging
from . import models from . import models
from .plugins import default_plugin_type
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -13,8 +12,8 @@ class Client(object):
def analyse(self, input, method='GET', **kwargs): def analyse(self, input, method='GET', **kwargs):
return self.request('/', method=method, input=input, **kwargs) return self.request('/', method=method, input=input, **kwargs)
def plugins(self, ptype=default_plugin_type): def plugins(self, *args, **kwargs):
resp = self.request(path='/plugins', plugin_type=ptype).plugins resp = self.request(path='/plugins').plugins
return {p.name: p for p in resp} return {p.name: p for p in resp}
def request(self, path=None, method='GET', **params): def request(self, path=None, method='GET', **params):

View File

@ -123,7 +123,7 @@ class Senpy(object):
return return
plugin = plugins[0] plugin = plugins[0]
self._activate(plugin) # Make sure the plugin is activated self._activate(plugin) # Make sure the plugin is activated
specific_params = api.get_extra_params(req, plugin) specific_params = api.parse_extra_params(req, plugin)
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)
@ -262,17 +262,11 @@ class Senpy(object):
with plugin._lock: with plugin._lock:
if plugin.is_activated: if plugin.is_activated:
return return
try:
plugin.activate() plugin.activate()
msg = "Plugin activated: {}".format(plugin.name) msg = "Plugin activated: {}".format(plugin.name)
logger.info(msg) logger.info(msg)
success = True success = True
self._set_active(plugin, success) self._set_active(plugin, success)
except Exception as ex:
msg = "Error activating plugin {} - {} : \n\t{}".format(
plugin.name, ex, traceback.format_exc())
logger.error(msg)
raise Error(msg)
def activate_plugin(self, plugin_name, sync=True): def activate_plugin(self, plugin_name, sync=True):
try: try:
@ -294,13 +288,8 @@ class Senpy(object):
with plugin._lock: with plugin._lock:
if not plugin.is_activated: if not plugin.is_activated:
return return
try:
plugin.deactivate() plugin.deactivate()
logger.info("Plugin deactivated: {}".format(plugin.name)) logger.info("Plugin deactivated: {}".format(plugin.name))
except Exception as ex:
logger.error(
"Error deactivating plugin {}: {}".format(plugin.name, ex))
logger.error("Trace: {}".format(traceback.format_exc()))
def deactivate_plugin(self, plugin_name, sync=True): def deactivate_plugin(self, plugin_name, sync=True):
try: try:

View File

@ -6,7 +6,11 @@ For compatibility with Py3 and for easier debugging, this new version drops
introspection and adds all arguments to the models. introspection and adds all arguments to the models.
''' '''
from __future__ import print_function from __future__ import print_function
from six import string_types from future import standard_library
standard_library.install_aliases()
from future.utils import with_metaclass
from past.builtins import basestring
import time import time
import copy import copy
@ -15,6 +19,8 @@ import os
import jsonref import jsonref
import jsonschema import jsonschema
import inspect import inspect
from collections import UserDict
from abc import ABCMeta
from flask import Response as FlaskResponse from flask import Response as FlaskResponse
from pyld import jsonld from pyld import jsonld
@ -62,7 +68,7 @@ class Context(dict):
return contexts return contexts
elif isinstance(context, dict): elif isinstance(context, dict):
return Context(context) return Context(context)
elif isinstance(context, string_types): elif isinstance(context, basestring):
try: try:
with open(context) as f: with open(context) as f:
return Context(json.loads(f.read())) return Context(json.loads(f.read()))
@ -75,9 +81,154 @@ class Context(dict):
base_context = Context.load(CONTEXT_PATH) base_context = Context.load(CONTEXT_PATH)
class SenpyMixin(object): class BaseMeta(ABCMeta):
'''
Metaclass for models. It extracts the default values for the fields in
the model.
For instance, instances of the following class wouldn't need to mark
their version or description on initialization:
.. code-block:: python
class MyPlugin(Plugin):
version=0.3
description='A dull plugin'
Note that these operations could be included in the __init__ of the
class, but it would be very inefficient.
'''
def __new__(mcs, name, bases, attrs, **kwargs):
defaults = {}
if 'schema' in attrs:
defaults = mcs.get_defaults(attrs['schema'])
for b in bases:
if hasattr(b, 'defaults'):
defaults.update(b.defaults)
info = mcs.attrs_to_dict(attrs)
defaults.update(info)
attrs['defaults'] = defaults
return super(BaseMeta, mcs).__new__(mcs, name, bases, attrs)
@staticmethod
def attrs_to_dict(attrs):
'''
Extract the attributes of the class.
This allows adding default values in the class definition.
e.g.:
'''
def is_attr(k, v):
return (not(inspect.isroutine(v) or
inspect.ismethod(v) or
inspect.ismodule(v) or
isinstance(v, property)) and
k[0] != '_' and
k != 'schema' and
k != 'data')
return {key: copy.deepcopy(value) for key, value in attrs.items() if is_attr(key, value)}
@staticmethod
def get_defaults(schema):
temp = {}
for obj in [
schema,
] + schema.get('allOf', []):
for k, v in obj.get('properties', {}).items():
if 'default' in v and k not in temp:
temp[k] = copy.deepcopy(v['default'])
return temp
class CustomDict(UserDict, object):
'''
A dictionary whose elements can also be accessed as attributes. Since some
characters are not valid in the dot-notation, the attribute names also
converted. e.g.:
> d = CustomDict()
> d.key = d['ns:name'] = 1
> d.key == d['key']
True
> d.ns__name == d['ns:name']
'''
defaults = []
def __init__(self, *args, **kwargs):
temp = copy.deepcopy(self.defaults)
for arg in args:
temp.update(copy.deepcopy(arg))
for k, v in kwargs.items():
temp[self._get_key(k)] = v
super(CustomDict, self).__init__(temp)
@staticmethod
def _get_key(key):
if key is 'id':
key = '@id'
key = key.replace("__", ":", 1)
return key
@staticmethod
def _internal_key(key):
return key[0] == '_' or key == 'data'
def __getattr__(self, key):
'''
__getattr__ only gets called when the attribute could not be found
in the __dict__. So we only need to look for the the element in the
dictionary, or raise an Exception.
'''
mkey = self._get_key(key)
if not self._internal_key(key) and mkey in self:
return self[mkey]
raise AttributeError(key)
def __setattr__(self, key, value):
# Work as usual for internal properties or already existing
# properties
if self._internal_key(key) or key in self.__dict__:
return super(CustomDict, self).__setattr__(key, value)
key = self._get_key(key)
return self.__setitem__(self._get_key(key), value)
def __delattr__(self, key):
if self._internal_key(key):
return object.__delattr__(self, key)
key = self._get_key(key)
self.__delitem__(self._get_key(key))
class BaseModel(with_metaclass(BaseMeta, CustomDict)):
'''
Entities of the base model are a special kind of dictionary that emulates
a JSON-LD object. The structure of the dictionary is checked via JSON-schema.
For convenience, the values can also be accessed as attributes
(a la Javascript). e.g.:
> myobject.key == myobject['key']
True
> myobject.ns__name == myobject['ns:name']
True
'''
schema = base_schema
_context = base_context["@context"] _context = base_context["@context"]
def __init__(self, *args, **kwargs):
auto_id = kwargs.pop('_auto_id', True)
super(BaseModel, self).__init__(*args, **kwargs)
if '@id' not in self and auto_id:
self.id = ':{}_{}'.format(type(self).__name__, time.time())
if '@type' not in self:
logger.warn('Created an instance of an unknown model')
def flask(self, def flask(self,
in_headers=True, in_headers=True,
headers=None, headers=None,
@ -146,7 +297,7 @@ class SenpyMixin(object):
else: else:
return item return item
return ser_or_down(self._plain_dict()) return ser_or_down(self.data)
def jsonld(self, def jsonld(self,
with_context=True, with_context=True,
@ -188,150 +339,21 @@ class SenpyMixin(object):
return str(self.serialize()) return str(self.serialize())
class BaseModel(SenpyMixin, dict): _subtypes = {}
'''
Entities of the base model are a special kind of dictionary that emulates
a JSON-LD object. For convenience, the values can also be accessed as attributes
(a la Javascript). e.g.:
> myobject.key == myobject['key']
True
> myobject.ns__name == myobject['ns:name']
True
'''
schema = base_schema
def __init__(self, *args, **kwargs):
self.attrs_to_dict()
if 'id' in kwargs:
self.id = kwargs.pop('id')
elif kwargs.pop('_auto_id', True):
self.id = '_:{}_{}'.format(type(self).__name__, time.time())
temp = self.get_defaults()
temp.update(dict(*args))
for k, v in kwargs.items():
temp[self._get_key(k)] = v
super(BaseModel, self).__init__(temp)
if '@type' not in self:
logger.warn('Created an instance of an unknown model')
def get_defaults(self):
temp = {}
for obj in [
self.schema,
] + self.schema.get('allOf', []):
for k, v in obj.get('properties', {}).items():
if 'default' in v and k not in temp:
temp[k] = copy.deepcopy(v['default'])
return temp
def attrs_to_dict(self):
'''
Copy the attributes of the class to the instance.
This allows adding default values in the class definition.
e.g.:
class MyPlugin(Plugin):
version=0.3
description='A dull plugin'
'''
def is_attr(x):
return not(inspect.isroutine(x) or inspect.ismethod(x) or isinstance(x, property))
for key, value in inspect.getmembers(self.__class__, is_attr):
if key[0] != '_' and key != 'schema':
self[key] = value
def _get_key(self, key):
if key is 'id':
key = '@id'
key = key.replace("__", ":", 1)
return key
def __delitem__(self, key):
key = self._get_key(key)
dict.__delitem__(self, key)
def _internal_key(self, key):
return key[0] == '_' or key in self.__dict__
def _plain_dict(self):
d = {k: v for (k, v) in self.items() if k[0] != "_"}
return d
def __getattr__(self, key):
'''
__getattr__ only gets called when the attribute could not
be found in the __dict__. So we only need to look for the
the element in the dictionary, or raise an Exception.
'''
if self._internal_key(key):
raise AttributeError(key)
return self.__getitem__(self._get_key(key))
def __setattr__(self, key, value):
if self._internal_key(key):
return super(BaseModel, self).__setattr__(key, value)
key = self._get_key(key)
return self.__setitem__(self._get_key(key), value)
def __delattr__(self, key):
if self._internal_key(key):
return object.__delattr__(self, key)
key = self._get_key(key)
self.__delitem__(self._get_key(key))
def register(rsubclass, rtype=None): def register(rsubclass, rtype=None):
_subtypes[rtype or rsubclass.__name__] = rsubclass _subtypes[rtype or rsubclass.__name__] = rsubclass
_subtypes = {}
def from_dict(indict, cls=None):
if not cls:
target = indict.get('@type', None)
try:
if target and target in _subtypes:
cls = _subtypes[target]
else:
cls = BaseModel
except Exception:
cls = BaseModel
outdict = dict()
for k, v in indict.items():
if k == '@context':
pass
elif isinstance(v, dict):
v = from_dict(indict[k])
elif isinstance(v, list):
for ix, v2 in enumerate(v):
if isinstance(v2, dict):
v[ix] = from_dict(v2)
outdict[k] = v
return cls(**outdict)
def from_string(string, **kwargs):
return from_dict(json.loads(string), **kwargs)
def from_json(injson):
indict = json.loads(injson)
return from_dict(indict)
def from_schema(name, schema=None, schema_file=None, base_classes=None): def from_schema(name, schema=None, schema_file=None, base_classes=None):
base_classes = base_classes or [] base_classes = base_classes or []
base_classes.append(BaseModel) base_classes.append(BaseModel)
schema_file = schema_file or '{}.json'.format(name) schema_file = schema_file or '{}.json'.format(name)
class_name = '{}{}'.format(name[0].upper(), name[1:]) class_name = '{}{}'.format(name[0].upper(), name[1:])
if '/' not in 'schema_file': if '/' not in 'schema_file':
schema_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), thisdir = os.path.dirname(os.path.realpath(__file__))
schema_file = os.path.join(thisdir,
'schemas', 'schemas',
schema_file) schema_file)
@ -354,6 +376,40 @@ def from_schema(name, schema=None, schema_file=None, base_classes=None):
return newclass return newclass
def from_dict(indict, cls=None):
if not cls:
target = indict.get('@type', None)
try:
if target and target in _subtypes:
cls = _subtypes[target]
else:
cls = BaseModel
except Exception:
cls = BaseModel
outdict = dict()
for k, v in indict.items():
if k == '@context':
pass
elif isinstance(v, dict):
v = from_dict(indict[k])
elif isinstance(v, list):
v = v[:]
for ix, v2 in enumerate(v):
if isinstance(v2, dict):
v[ix] = from_dict(v2)
outdict[k] = copy.deepcopy(v)
return cls(**outdict)
def from_string(string, **kwargs):
return from_dict(json.loads(string), **kwargs)
def from_json(injson):
indict = json.loads(injson)
return from_dict(indict)
def _add_from_schema(*args, **kwargs): def _add_from_schema(*args, **kwargs):
generatedClass = from_schema(*args, **kwargs) generatedClass = from_schema(*args, **kwargs)
globals()[generatedClass.__name__] = generatedClass globals()[generatedClass.__name__] = generatedClass
@ -384,40 +440,14 @@ for i in [
_ErrorModel = from_schema('error') _ErrorModel = from_schema('error')
class Error(SenpyMixin, Exception): class Error(_ErrorModel, Exception):
def __init__(self, message, *args, **kwargs): def __init__(self, message, *args, **kwargs):
super(Error, self).__init__(self, message, message) Exception.__init__(self, message)
self._error = _ErrorModel(message=message, *args, **kwargs) super(Error, self).__init__(*args, **kwargs)
self.message = message self.message = message
def validate(self, obj=None): def __hash__(self):
self._error.validate() return Exception.__hash__(self)
def __getitem__(self, key):
return self._error[key]
def __setitem__(self, key, value):
self._error[key] = value
def __delitem__(self, key):
del self._error[key]
def __getattr__(self, key):
if key != '_error' and hasattr(self._error, key):
return getattr(self._error, key)
raise AttributeError(key)
def __setattr__(self, key, value):
if key != '_error':
return setattr(self._error, key, value)
else:
super(Error, self).__setattr__(key, value)
def __delattr__(self, key):
delattr(self._error, key)
def __str__(self):
return str(self.to_JSON(with_context=False))
register(Error, 'error') register(Error, 'error')

View File

@ -1,5 +1,6 @@
from future import standard_library from future import standard_library
standard_library.install_aliases() standard_library.install_aliases()
from future.utils import with_metaclass
import os.path import os.path
import os import os
@ -16,21 +17,33 @@ import yaml
import threading import threading
from .. import models, utils from .. import models, utils
from ..api import API_PARAMS from .. import api
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Plugin(models.Plugin): class PluginMeta(models.BaseMeta):
def __init__(self, info=None, data_folder=None):
def __new__(mcs, name, bases, attrs, **kwargs):
plugin_type = []
if hasattr(bases[0], 'plugin_type'):
plugin_type += bases[0].plugin_type
plugin_type.append(name)
attrs['plugin_type'] = plugin_type
return super(PluginMeta, mcs).__new__(mcs, name, bases, attrs)
class Plugin(with_metaclass(PluginMeta, models.Plugin)):
def __init__(self, info=None, data_folder=None, **kwargs):
""" """
Provides a canonical name for plugins and serves as base for other Provides a canonical name for plugins and serves as base for other
kinds of plugins. kinds of plugins.
""" """
logger.debug("Initialising {}".format(info)) logger.debug("Initialising {}".format(info))
super(Plugin, self).__init__(**kwargs)
if info: if info:
self.update(info) self.update(info)
super(Plugin, self).__init__(**self)
if not self.validate(): if not self.validate():
raise models.Error(message=("You need to provide configuration" raise models.Error(message=("You need to provide configuration"
"information for the plugin.")) "information for the plugin."))
@ -57,7 +70,8 @@ class Plugin(models.Plugin):
'test cases').format(self.id, inspect.getfile(self.__class__))) 'test cases').format(self.id, inspect.getfile(self.__class__)))
for case in self.test_cases: for case in self.test_cases:
entry = models.Entry(case['entry']) entry = models.Entry(case['entry'])
params = case.get('params', {}) given_parameters = case.get('params', {})
params = api.parse_params(given_parameters, self.extra_params)
fails = case.get('fails', False) fails = case.get('fails', False)
try: try:
res = list(self.analyse_entry(entry, params)) res = list(self.analyse_entry(entry, params))
@ -90,7 +104,7 @@ SenpyPlugin = Plugin
class AnalysisPlugin(Plugin): class AnalysisPlugin(Plugin):
def analyse(self, *args, **kwargs): def analyse(self, *args, **kwargs):
raise NotImplemented( raise NotImplementedError(
'Your method should implement either analyse or analyse_entry') 'Your method should implement either analyse or analyse_entry')
def analyse_entry(self, entry, parameters): def analyse_entry(self, entry, parameters):
@ -118,17 +132,17 @@ class ConversionPlugin(Plugin):
pass pass
class SentimentPlugin(models.SentimentPlugin, AnalysisPlugin): class SentimentPlugin(AnalysisPlugin, models.SentimentPlugin):
minPolarityValue = 0 minPolarityValue = 0
maxPolarityValue = 1 maxPolarityValue = 1
class EmotionPlugin(models.EmotionPlugin, AnalysisPlugin): class EmotionPlugin(AnalysisPlugin, models.EmotionPlugin):
minEmotionValue = 0 minEmotionValue = 0
maxEmotionValue = 1 maxEmotionValue = 1
class EmotionConversionPlugin(models.EmotionConversionPlugin, ConversionPlugin): class EmotionConversionPlugin(ConversionPlugin):
pass pass
@ -171,19 +185,18 @@ class ShelfMixin(object):
pickle.dump(self._sh, f) pickle.dump(self._sh, f)
default_plugin_type = API_PARAMS['plugin_type']['default']
def pfilter(plugins, **kwargs): def pfilter(plugins, **kwargs):
""" Filter plugins by different criteria """ """ Filter plugins by different criteria """
if isinstance(plugins, models.Plugins): if isinstance(plugins, models.Plugins):
plugins = plugins.plugins plugins = plugins.plugins
elif isinstance(plugins, dict): elif isinstance(plugins, dict):
plugins = plugins.values() plugins = plugins.values()
ptype = kwargs.pop('plugin_type', default_plugin_type) ptype = kwargs.pop('plugin_type', AnalysisPlugin)
logger.debug('#' * 100) logger.debug('#' * 100)
logger.debug('ptype {}'.format(ptype)) logger.debug('ptype {}'.format(ptype))
if ptype: if ptype:
if isinstance(ptype, PluginMeta):
ptype = ptype.__name__
try: try:
ptype = ptype[0].upper() + ptype[1:] ptype = ptype[0].upper() + ptype[1:]
pclass = globals()[ptype] pclass = globals()[ptype]

View File

@ -4,7 +4,7 @@ from senpy.plugins import EmotionPlugin
from senpy.models import EmotionSet, Emotion, Entry from senpy.models import EmotionSet, Emotion, Entry
class RmoRandPlugin(EmotionPlugin): class EmoRandPlugin(EmotionPlugin):
def analyse_entry(self, entry, params): def analyse_entry(self, entry, params):
category = "emoml:big6happiness" category = "emoml:big6happiness"
number = max(-1, min(1, random.gauss(0, 0.5))) number = max(-1, min(1, random.gauss(0, 0.5)))

View File

@ -11,7 +11,7 @@ class SplitPlugin(AnalysisPlugin):
nltk.download('punkt') nltk.download('punkt')
def analyse_entry(self, entry, params): def analyse_entry(self, entry, params):
chunker_type = params.get("delimiter", "sentence") chunker_type = params["delimiter"]
original_text = entry['nif:isString'] original_text = entry['nif:isString']
if chunker_type == "sentence": if chunker_type == "sentence":
tokenizer = PunktSentenceTokenizer() tokenizer = PunktSentenceTokenizer()

View File

@ -7,7 +7,7 @@ from senpy.models import Sentiment
class Sentiment140Plugin(SentimentPlugin): class Sentiment140Plugin(SentimentPlugin):
def analyse_entry(self, entry, params): def analyse_entry(self, entry, params):
lang = params.get("language", "auto") lang = params["language"]
res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson", res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson",
json.dumps({ json.dumps({
"language": lang, "language": lang,
@ -35,6 +35,18 @@ class Sentiment140Plugin(SentimentPlugin):
entry.language = lang entry.language = lang
yield entry yield entry
def test(self, *args, **kwargs):
'''
To avoid calling the sentiment140 API, we will mock the results
from requests.
'''
from senpy.test import patch_requests
expected = {"data": [{"polarity": 10}]}
with patch_requests(expected) as (request, response):
super(Sentiment140Plugin, self).test(*args, **kwargs)
assert request.called
assert response.json.called
test_cases = [ test_cases = [
{ {
'entry': { 'entry': {

View File

@ -16,6 +16,7 @@ extra_params:
- es - es
- en - en
- auto - auto
default: auto
requirements: {} requirements: {}
maxPolarityValue: 1 maxPolarityValue: 1
minPolarityValue: 0 minPolarityValue: 0

View File

@ -7,11 +7,11 @@
"description": "Help containing accepted parameters", "description": "Help containing accepted parameters",
"type": "object", "type": "object",
"properties": { "properties": {
"parameters": { "valid_parameters": {
"type": "object" "type": "object"
} }
}, },
"required": "parameters" "required": "valid_parameters"
} }
] ]
} }

View File

@ -1,7 +1,7 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"type": "object", "type": "object",
"required": ["@id", "extra_params"], "required": ["@id", "name", "description", "version", "plugin_type"],
"properties": { "properties": {
"@id": { "@id": {
"type": "string", "type": "string",
@ -9,7 +9,19 @@
}, },
"name": { "name": {
"type": "string", "type": "string",
"description": "The name of the plugin, which will be used in the algorithm detection phase" "description": "The name of the plugin, which will be used in the algorithm detection phase."
},
"description": {
"type": "string",
"description": "A summary of what the plugin does, and pointers to further information."
},
"version": {
"type": "string",
"description": "The version of the plugin."
},
"plugin_type": {
"type": "string",
"description": "Sub-type of plugin. e.g. sentimentPlugin"
}, },
"extra_params": { "extra_params": {
"type": "object", "type": "object",

View File

@ -152,3 +152,18 @@ textarea{
/* background: white; */ /* background: white; */
display: none; display: none;
} }
.deco-none {
color: inherit;
text-decoration: inherit;
}
.deco-none:link {
color: inherit;
text-decoration: inherit;
}
.deco-none:hover {
color: inherit;
text-decoration: inherit;
}

View File

@ -1,7 +1,10 @@
var ONYX = "http://www.gsi.dit.upm.es/ontologies/onyx/ns#"; var ONYX = "http://www.gsi.dit.upm.es/ontologies/onyx/ns#";
var RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"; var RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
var plugins_params={}; var plugins_params = default_params = {};
var default_params = JSON.parse($.ajax({type: "GET", url: "/api?help=true" , async: false}).responseText); var plugins = [];
var defaultPlugin = {};
var gplugins = {};
function replaceURLWithHTMLLinks(text) { function replaceURLWithHTMLLinks(text) {
console.log('Text: ' + text); console.log('Text: ' + text);
var exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig; var exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
@ -25,39 +28,25 @@ function hashchanged(){
} }
} }
$(document).ready(function() {
var response = JSON.parse($.ajax({type: "GET", url: "/api/plugins/" , async: false}).responseText); function get_plugins(response){
var defaultPlugin= JSON.parse($.ajax({type: "GET", url: "/api/plugins/default" , async: false}).responseText);
html="";
var availablePlugins = document.getElementById('availablePlugins');
plugins = response.plugins; plugins = response.plugins;
gplugins = {}; }
function group_plugins(){
for (r in plugins){ for (r in plugins){
ptype = plugins[r]['@type']; ptype = plugins[r]['@type'];
if(gplugins[ptype] == undefined){ if(gplugins[ptype] == undefined){
gplugins[ptype] = [r] gplugins[ptype] = [r];
}else{ }else{
gplugins[ptype].push(r) gplugins[ptype].push(r);
}
} }
} }
for (g in gplugins){
html += "<optgroup label=\""+g+"\">"
for (r in gplugins[g]){
plugin = plugins[gplugins[g][r]]
if (!plugin["name"]){
console.log("No name for plugin ", plugin);
continue;
}
html+= "<option value=\""+plugin["name"]+"\" "
if (plugin["name"] == defaultPlugin["name"]){
html+= " selected=\"selected\""
}
if (!plugin["is_activated"]){
html+= " disabled=\"disabled\" "
}
html+=">"+plugin["name"]+"</option>"
function get_parameters(){
for (p in plugins){
plugin = plugins[p];
if (plugin["extra_params"]){ if (plugin["extra_params"]){
plugins_params[plugin["name"]]={}; plugins_params[plugin["name"]]={};
for (param in plugin["extra_params"]){ for (param in plugin["extra_params"]){
@ -73,8 +62,41 @@ $(document).ready(function() {
} }
} }
} }
var pluginEntry = document.createElement('li'); }
function draw_plugins_selection(){
html="";
group_plugins();
for (g in gplugins){
html += "<optgroup label=\""+g+"\">"
for (r in gplugins[g]){
plugin = plugins[gplugins[g][r]]
if (!plugin["name"]){
console.log("No name for plugin ", plugin);
continue;
}
html+= "<option value=\""+plugin.name+"\" "
if (plugin["name"] == defaultPlugin["name"]){
html+= " selected=\"selected\""
}
if (!plugin["is_activated"]){
html+= " disabled=\"disabled\" "
}
html+=">"+plugin["name"]+"</option>"
}
}
html += "</optgroup>"
document.getElementById('plugins').innerHTML = html;
}
function draw_plugins_list(){
var availablePlugins = document.getElementById('availablePlugins');
for(p in plugins){
var pluginEntry = document.createElement('li');
plugin = plugins[p];
newHtml = "" newHtml = ""
if(plugin.url) { if(plugin.url) {
newHtml= "<a href="+plugin.url+">" + plugin.name + "</a>"; newHtml= "<a href="+plugin.url+">" + plugin.name + "</a>";
@ -85,9 +107,17 @@ $(document).ready(function() {
pluginEntry.innerHTML = newHtml; pluginEntry.innerHTML = newHtml;
availablePlugins.appendChild(pluginEntry) availablePlugins.appendChild(pluginEntry)
} }
html += "</optgroup>" }
document.getElementById('plugins').innerHTML = html;
change_params(); $(document).ready(function() {
var response = JSON.parse($.ajax({type: "GET", url: "/api/plugins/" , async: false}).responseText);
defaultPlugin= JSON.parse($.ajax({type: "GET", url: "/api/plugins/default" , async: false}).responseText);
get_plugins(response);
get_default_parameters();
draw_plugins_list();
draw_plugins_selection();
draw_parameters();
$(window).on('hashchange', hashchanged); $(window).on('hashchange', hashchanged);
hashchanged(); hashchanged();
@ -95,49 +125,131 @@ $(document).ready(function() {
}); });
function get_default_parameters(){
default_params = JSON.parse($.ajax({type: "GET", url: "/api?help=true" , async: false}).responseText).valid_parameters;
// Remove the parameters that are always added
delete default_params["input"];
delete default_params["algorithm"];
delete default_params["help"];
function change_params(){ }
function draw_default_parameters(){
var basic_params = document.getElementById("basic_params");
basic_params.innerHTML = params_div(default_params);
}
function draw_extra_parameters(){
var plugin = document.getElementById("plugins").options[document.getElementById("plugins").selectedIndex].value; var plugin = document.getElementById("plugins").options[document.getElementById("plugins").selectedIndex].value;
html="" get_parameters();
for (param in default_params){
if ((default_params[param]['options']) && (['help','conversion'].indexOf(param) < 0)){
html+= "<label> "+param+"</label>"
if (default_params[param]['options'].length < 1) {
html +="<input></input>";
}
else {
html+= "<select id=\""+param+"\" name=\""+param+"\">"
for (option in default_params[param]['options']){
if (default_params[param]['options'][option] == default_params[param]['default']){
html+="<option value \""+default_params[param]['options'][option]+"\" selected >"+default_params[param]['options'][option]+"</option>"
}
else{
html+="<option value \""+default_params[param]['options'][option]+"\">"+default_params[param]['options'][option]+"</option>"
var extra_params = document.getElementById("extra_params");
extra_params.innerHTML = params_div(plugins_params[plugin]);
} }
function draw_parameters(){
draw_default_parameters();
draw_extra_parameters();
} }
function add_default_params(){
var html = "";
// html += '<a href="#basic_params" class="btn btn-info" data-toggle="collapse">Basic API parameters</a>';
html += '<span id="basic_params" class="panel-collapse collapse">';
html += '<ul class="list-group">'
html += params_div(default_params);
html += '</span>';
return html;
} }
html+="</select><br>"
function params_div(params){
var html = '<div class="container-fluid">';
if (Object.keys(params).length === 0) {
html += '<p class="text text-muted text-center">This plugin does not take any extra parameters</p>';
} }
// Iterate over the keys in order
pnames = Object.keys(params).sort()
for (ix in pnames){
pname = pnames[ix];
param = params[pname];
html+='<div class="form-group">';
html += '<div class="row">'
html+= '<label class="col-sm-2" for="'+pname+'">'+pname+'</label>'
if (param.options){
opts = param.options;
if(param.options.length == 1 && param.options[0] == 'boolean') {
opts = [true, false];
} }
for (param in plugins_params[plugin]){ html+= '<select class="col-sm-10" id="'+pname+"\" name=\""+pname+"\">"
if (param || plugins_params[plugin][param].length > 1){ var defaultopt = param.default;
html+= "<label> Parameter "+param+"</label>" for (option in opts){
param_opts = plugins_params[plugin][param] isselected = "";
if (param_opts.length > 0) { if (defaultopt != undefined && opts[option] == defaultopt ){
html+= "<select id=\""+param+"\" name=\""+param+"\">" isselected = ' selected="selected"'
for (option in param_opts){ }
html+="<option value \""+param_opts[option]+"\">"+param_opts[option]+"</option>" html+="<option value=\""+opts[option]+'"' + isselected +
'>'+opts[option]+"</option>"
} }
html+="</select>" html+="</select>"
} }
else { else {
html +="<input id=\""+param+"\" name=\""+param+"\"></input>"; default_value = "";
} if(param.default != undefined){
} default_value = param.default;
}
document.getElementById("params").innerHTML = html
}; };
html +='<input class="col-sm-10" id="'+pname+'" name="'+pname+'" value="' + default_value + '"></input>';
}
html+='</div>';
html+='<div class="row">';
if ('description' in param){
html += '<p class="form-text sm-sm-12 text-muted text-center">' + param.description + '</p>';
}
html+='</div>';
html+='</div>';
}
html+='</div>';
return html;
}
function _get_form_parameters(id){
var element = document.getElementById(id);
params = {};
var selects = element.getElementsByTagName('select');
var inputs = element.getElementsByTagName('input');
Array.prototype.forEach.call(selects, function (sel) {
key = sel.name;
value = sel.options[sel.selectedIndex].value
params[key] = value;
});
Array.prototype.forEach.call(inputs, function (el) {
params[el.name] = el.value;
});
for (k in params){
value = params[k];
if (value == "" || value === "undefined"){
delete params[k];
}
}
return params;
}
function get_form_parameters(){
var p1 = _get_form_parameters("basic_params");
var p2 = _get_form_parameters("extra_params");
return Object.assign(p1, p2);
}
function add_param(key, value){
value = encodeURIComponent(value);
return "&"+key+"="+value;
}
function load_JSON(){ function load_JSON(){
url = "/api"; url = "/api";
@ -148,31 +260,15 @@ function load_JSON(){
var plugin = document.getElementById("plugins").options[document.getElementById("plugins").selectedIndex].value; var plugin = document.getElementById("plugins").options[document.getElementById("plugins").selectedIndex].value;
var input = encodeURIComponent(document.getElementById("input").value); var input = encodeURIComponent(document.getElementById("input").value);
url += "?algo="+plugin+"&i="+input url += "?algo="+plugin+"&i="+input
for (param in plugins_params[plugin]){
if (param != null){
field = document.getElementById(param);
if (plugins_params[plugin][param].length > 0){
var param_value = encodeURIComponent(field.options[field.selectedIndex].text);
} else {
var param_value = encodeURIComponent(field.text);
}
if (param_value !== "undefined" && param_value.length > 0){
url+="&"+param+"="+param_value
}
}
}
for (param in default_params){ params = get_form_parameters();
if ((param != null) && (default_params[param]['options']) && (['help','conversion'].indexOf(param) < 0)){
var param_value = encodeURIComponent(document.getElementById(param).options[document.getElementById(param).selectedIndex].value); for (key in params){
if (param_value){ url += add_param(key, params[key]);
url+="&"+param+"="+param_value
}
}
} }
var response = $.ajax({type: "GET", url: url , async: false}).responseText; var response = $.ajax({type: "GET", url: url , async: false}).responseText;
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response) rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
document.getElementById("input_request").innerHTML = "<a href='"+url+"'>"+url+"</a>" document.getElementById("input_request").innerHTML = "<a href='"+url+"'>"+url+"</a>"
document.getElementById("results-div").style.display = 'block'; document.getElementById("results-div").style.display = 'block';
@ -183,12 +279,13 @@ function load_JSON(){
}; };
var editor = new JSONEditor(container, options, response); var editor = new JSONEditor(container, options, response);
editor.expandAll(); editor.expandAll();
// $('#results-div a[href="#viewer"]').tab('show');
$('#results-div a[href="#viewer"]').click();
// location.hash = 'raw';
} }
catch(err){ catch(err){
console.log("Error decoding JSON (got turtle?)"); console.log("Error decoding JSON (got turtle?)");
$('#results-div a[href="#raw"]').click();
// location.hash = 'raw';
} }
} }

View File

@ -88,28 +88,52 @@
<div class="tab-pane active" id="test"> <div class="tab-pane active" id="test">
<div class="well"> <div class="well">
<form id="form" onsubmit="return getPlugins();" accept-charset="utf-8"> <form id="form" class="container" onsubmit="return getPlugins();" accept-charset="utf-8">
<div id="inputswrapper">
<div><textarea id="input" class="boxsizingBorder" rows="5" name="i">This text makes me sad. <div><textarea id="input" class="boxsizingBorder" rows="5" name="i">This text makes me sad.
whilst this text makes me happy and surprised at the same time. whilst this text makes me happy and surprised at the same time.
I cannot believe it!</textarea></div> I cannot believe it!</textarea>
<label>Select the plugin:</label>
<select id="plugins" name="plugins" onchange="change_params()">
</select>
</br>
<div id ="params">
</div> </div>
</br> <div>
<label>Select the plugin:</label>
<select id="plugins" name="plugins" onchange="draw_extra_parameters()">
</select>
</div>
<!-- PARAMETERS -->
<div class="panel-group" id="parameters">
<div class="panel panel-default">
<a data-toggle="collapse" class="deco-none" href="#basic_params">
<div class="panel-heading">
<h4 class="panel-title">
Basic API parameters
</h4>
</div>
</a>
<div id="basic_params" class="panel-collapse collapse panel-body">
</div>
</div>
<div class="panel panel-default">
<a data-toggle="collapse" class="deco-none" href="#extra_params">
<div class="panel-heading">
<h4 class="panel-title">
Plugin extra parameters
</h4>
</div>
</a>
<div id="extra_params" class="panel-collapse collapse in panel-body">
</div>
</div>
</div>
<!-- END PARAMETERS -->
<a id="preview" class="btn btn-lg btn-primary" onclick="load_JSON()">Analyse!</a> <a id="preview" class="btn btn-lg btn-primary" onclick="load_JSON()">Analyse!</a>
<!--<button id="visualise" name="type" type="button">Visualise!</button>--> <!--<button id="visualise" name="type" type="button">Visualise!</button>-->
</div>
</form> </form>
</div> </div>
<span id="input_request"></span> <span id="input_request"></span>
<div id="results-div"> <div id="results-div">
<ul class="nav nav-tabs" role="tablist"> <ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a class="active" href="#viewer">Viewer</a></li> <li role="presentation" class="active"><a data-toggle="tab" class="active" href="#viewer">Viewer</a></li>
<li role="presentation"><a class="active" href="#raw">Raw</a></li> <li role="presentation"><a data-toggle="tab" class="active" href="#raw">Raw</a></li>
</ul> </ul>
<div class="tab-content" id="results-container"> <div class="tab-content" id="results-container">

43
senpy/test.py Normal file
View File

@ -0,0 +1,43 @@
try:
from unittest.mock import patch, MagicMock
except ImportError:
from mock import patch, MagicMock
import json
from contextlib import contextmanager
from .models import BaseModel
class Call(dict):
def __init__(self, obj):
self.obj = obj.serialize()
self.status_code = 200
self.content = self.json()
def json(self):
return json.loads(self.obj)
@contextmanager
def patch_requests(value, code=200):
success = MagicMock()
if isinstance(value, BaseModel):
value = value.jsonld()
data = json.dumps(value)
success.json.return_value = value
success.data.return_value = data
success.status_code = code
if hasattr(value, 'jsonld'):
success.content = value.jsonld()
else:
success.content = json.dumps(value)
method_mocker = MagicMock()
method_mocker.return_value = success
with patch.multiple('requests', request=method_mocker,
get=method_mocker, post=method_mocker):
yield method_mocker, success
assert method_mocker.called

View File

@ -1,15 +1,17 @@
from . import models from . import models
from collections import MutableMapping
# MutableMapping should be enough, but it causes problems with py2
DICTCLASSES = (MutableMapping, dict, models.BaseModel)
def check_template(indict, template): def check_template(indict, template):
if isinstance(template, dict) and isinstance(indict, dict): if isinstance(template, DICTCLASSES) and isinstance(indict, DICTCLASSES):
for k, v in template.items(): for k, v in template.items():
if k not in indict: if k not in indict:
return '{} not in {}'.format(k, indict) return '{} not in {}'.format(k, indict)
check_template(indict[k], v) check_template(indict[k], v)
elif isinstance(template, list) and isinstance(indict, list): elif isinstance(template, list) and isinstance(indict, list):
if len(indict) != len(template):
raise models.Error('Different size for {} and {}'.format(indict, template))
for e in template: for e in template:
found = False found = False
for i in indict: for i in indict:
@ -17,6 +19,7 @@ def check_template(indict, template):
check_template(i, e) check_template(i, e)
found = True found = True
except models.Error as ex: except models.Error as ex:
# raise
continue continue
if not found: if not found:
raise models.Error('{} not found in {}'.format(e, indict)) raise models.Error('{} not found in {}'.format(e, indict))

View File

@ -162,5 +162,5 @@ class BlueprintsTest(TestCase):
resp = self.client.get("/api/?help=true") resp = self.client.get("/api/?help=true")
self.assertCode(resp, 200) self.assertCode(resp, 200)
js = parse_resp(resp) js = parse_resp(resp)
assert "parameters" in js assert "valid_parameters" in js
assert "help" in js["parameters"] assert "help" in js["valid_parameters"]

View File

@ -1,24 +1,9 @@
from unittest import TestCase from unittest import TestCase
try:
from unittest.mock import patch
except ImportError:
from mock import patch
import json
from senpy.test import patch_requests
from senpy.client import Client from senpy.client import Client
from senpy.models import Results, Plugins, Error from senpy.models import Results, Plugins, Error
from senpy.plugins import AnalysisPlugin, default_plugin_type from senpy.plugins import AnalysisPlugin
class Call(dict):
def __init__(self, obj):
self.obj = obj.serialize()
self.status_code = 200
self.content = self.json()
def json(self):
return json.loads(self.obj)
class ModelsTest(TestCase): class ModelsTest(TestCase):
@ -29,20 +14,18 @@ class ModelsTest(TestCase):
def test_client(self): def test_client(self):
endpoint = 'http://dummy/' endpoint = 'http://dummy/'
client = Client(endpoint) client = Client(endpoint)
success = Call(Results()) with patch_requests(Results()) as (request, response):
with patch('requests.request', return_value=success) as patched:
resp = client.analyse('hello') resp = client.analyse('hello')
assert isinstance(resp, Results) assert isinstance(resp, Results)
patched.assert_called_with( request.assert_called_with(
url=endpoint + '/', method='GET', params={'input': 'hello'}) url=endpoint + '/', method='GET', params={'input': 'hello'})
error = Call(Error('Nothing')) with patch_requests(Error('Nothing')) as (request, response):
with patch('requests.request', return_value=error) as patched:
try: try:
client.analyse(input='hello', algorithm='NONEXISTENT') client.analyse(input='hello', algorithm='NONEXISTENT')
raise Exception('Exceptions should be raised. This is not golang') raise Exception('Exceptions should be raised. This is not golang')
except Error: except Error:
pass pass
patched.assert_called_with( request.assert_called_with(
url=endpoint + '/', url=endpoint + '/',
method='GET', method='GET',
params={'input': 'hello', params={'input': 'hello',
@ -54,12 +37,11 @@ class ModelsTest(TestCase):
plugins = Plugins() plugins = Plugins()
p1 = AnalysisPlugin({'name': 'AnalysisP1', 'version': 0, 'description': 'No'}) p1 = AnalysisPlugin({'name': 'AnalysisP1', 'version': 0, 'description': 'No'})
plugins.plugins = [p1, ] plugins.plugins = [p1, ]
success = Call(plugins) with patch_requests(plugins) as (request, response):
with patch('requests.request', return_value=success) as patched:
response = client.plugins() response = client.plugins()
assert isinstance(response, dict) assert isinstance(response, dict)
assert len(response) == 1 assert len(response) == 1
assert "AnalysisP1" in response assert "AnalysisP1" in response
patched.assert_called_with( request.assert_called_with(
url=endpoint + '/plugins', method='GET', url=endpoint + '/plugins', method='GET',
params={'plugin_type': default_plugin_type}) params={})

View File

@ -106,7 +106,7 @@ class ExtensionsTest(TestCase):
r2 = analyse(self.senpy, input="tupni", output="tuptuo") r2 = analyse(self.senpy, input="tupni", output="tuptuo")
assert r1.analysis[0] == "plugins/Dummy_0.1" assert r1.analysis[0] == "plugins/Dummy_0.1"
assert r2.analysis[0] == "plugins/Dummy_0.1" assert r2.analysis[0] == "plugins/Dummy_0.1"
assert r1.entries[0]['nif:iString'] == 'input' assert r1.entries[0]['nif:isString'] == 'input'
def test_analyse_jsonld(self): def test_analyse_jsonld(self):
""" Using a plugin with JSON-LD input""" """ Using a plugin with JSON-LD input"""
@ -130,7 +130,7 @@ class ExtensionsTest(TestCase):
output="tuptuo") output="tuptuo")
assert r1.analysis[0] == "plugins/Dummy_0.1" assert r1.analysis[0] == "plugins/Dummy_0.1"
assert r2.analysis[0] == "plugins/Dummy_0.1" assert r2.analysis[0] == "plugins/Dummy_0.1"
assert r1.entries[0]['nif:iString'] == 'input' assert r1.entries[0]['nif:isString'] == 'input'
def test_analyse_error(self): def test_analyse_error(self):
mm = mock.MagicMock() mm = mock.MagicMock()
@ -185,7 +185,7 @@ class ExtensionsTest(TestCase):
response = Results({ response = Results({
'analysis': [{'plugin': plugin}], 'analysis': [{'plugin': plugin}],
'entries': [Entry({ 'entries': [Entry({
'nif:iString': 'much ado about nothing', 'nif:isString': 'much ado about nothing',
'emotions': [eSet1] 'emotions': [eSet1]
})] })]
}) })

View File

@ -12,8 +12,8 @@ from senpy.models import (Emotion,
Error, Error,
Results, Results,
Sentiment, Sentiment,
SentimentPlugin,
Plugins, Plugins,
Plugin,
from_string, from_string,
from_dict) from_dict)
from senpy import plugins from senpy import plugins
@ -99,7 +99,7 @@ class ModelsTest(TestCase):
def test_plugins(self): def test_plugins(self):
self.assertRaises(Error, plugins.Plugin) self.assertRaises(Error, plugins.Plugin)
p = plugins.Plugin({"name": "dummy", p = plugins.SentimentPlugin({"name": "dummy",
"description": "I do nothing", "description": "I do nothing",
"version": 0, "version": 0,
"extra_params": { "extra_params": {
@ -111,7 +111,7 @@ class ModelsTest(TestCase):
}}) }})
c = p.jsonld() c = p.jsonld()
assert '@type' in c assert '@type' in c
assert c['@type'] == 'plugin' assert c['@type'] == 'sentimentPlugin'
assert 'info' not in c assert 'info' not in c
assert 'repo' not in c assert 'repo' not in c
assert 'extra_params' in c assert 'extra_params' in c
@ -173,13 +173,14 @@ class ModelsTest(TestCase):
def test_single_plugin(self): def test_single_plugin(self):
"""A response with a single plugin should still return a list""" """A response with a single plugin should still return a list"""
plugs = Plugins() plugs = Plugins()
p = Plugin({'id': str(1), p = SentimentPlugin({'id': str(1),
'version': 0, 'version': 0,
'description': 'dummy'}) 'description': 'dummy'})
plugs.plugins.append(p) plugs.plugins.append(p)
assert isinstance(plugs.plugins, list) assert isinstance(plugs.plugins, list)
js = plugs.jsonld() js = plugs.jsonld()
assert isinstance(js['plugins'], list) assert isinstance(js['plugins'], list)
assert js['plugins'][0]['@type'] == 'sentimentPlugin'
def test_from_string(self): def test_from_string(self):
results = { results = {
@ -192,6 +193,7 @@ class ModelsTest(TestCase):
}] }]
} }
recovered = from_dict(results) recovered = from_dict(results)
assert recovered.id == results['@id']
assert isinstance(recovered, Results) assert isinstance(recovered, Results)
assert isinstance(recovered.entries[0], Entry) assert isinstance(recovered.entries[0], Entry)

View File

@ -6,7 +6,7 @@ import shutil
import tempfile import tempfile
from unittest import TestCase from unittest import TestCase
from senpy.models import Results, Entry, EmotionSet, Emotion from senpy.models import Results, Entry, EmotionSet, Emotion, Plugins
from senpy import plugins from senpy import plugins
from senpy.plugins.conversion.emotion.centroids import CentroidConversion from senpy.plugins.conversion.emotion.centroids import CentroidConversion
@ -49,6 +49,25 @@ class PluginsTest(TestCase):
assert os.path.isfile(a.shelf_file) assert os.path.isfile(a.shelf_file)
os.remove(a.shelf_file) os.remove(a.shelf_file)
def test_plugin_filter(self):
ps = Plugins()
for i in (plugins.SentimentPlugin,
plugins.EmotionPlugin,
plugins.AnalysisPlugin):
p = i(name='Plugin_{}'.format(i.__name__),
description='TEST',
version=0,
author='NOBODY')
ps.plugins.append(p)
assert len(ps.plugins) == 3
cases = [('AnalysisPlugin', 3),
('SentimentPlugin', 1),
('EmotionPlugin', 1)]
for name, num in cases:
res = plugins.pfilter(ps.plugins, plugin_type=name)
assert len(res) == num
def test_shelf(self): def test_shelf(self):
''' A shelf is created and the value is stored ''' ''' A shelf is created and the value is stored '''
newfile = self.shelf_file + "new" newfile = self.shelf_file + "new"