1
0
mirror of https://github.com/gsi-upm/senpy synced 2025-09-17 20:12:22 +00:00

Compare commits

..

12 Commits

Author SHA1 Message Date
Daniel Suarez Souto
530982be62 Fixed some code problems 2018-09-28 11:36:53 +02:00
Daniel Suarez Souto
38b478890b Added maxSentiment plugin 2018-09-28 11:11:47 +02:00
J. Fernando Sánchez
6dd4a44924 Make algorithm part of the URI
This also includes a couple of changes URIs to pass the tests with python 3.7

Closes #50
2018-08-17 11:01:56 +02:00
J. Fernando Sánchez
4291c5eabf Fix typo in requirements 2018-07-23 19:19:05 +02:00
J. Fernando Sánchez
7c7a815d1a Add *responses* to improve mocking 2018-07-23 19:07:57 +02:00
J. Fernando Sánchez
a3eb8f196c Several changes
* Add flag to run tests (and exit, or run the server)
* Add ntriples outformat
* Modify dependency installation logic to avoid installing several times
* Add encoded URLs as base/prefix
* Allow plugin activation to fail
2018-07-04 16:24:42 +02:00
J. Fernando Sánchez
00ffbb3804 Several changes
* Add flag to run tests
* Add ntriples outformat
2018-07-04 16:14:09 +02:00
J. Fernando Sánchez
13cf0c71c5 WIP
* Modify dependency installation logic (avoid installing several times)
* Add encoded URLs for as base/prefix
2018-06-28 18:24:18 +02:00
J. Fernando Sánchez
e5662d482e Allow activation fails 2018-06-20 11:51:06 +02:00
J. Fernando Sánchez
61181db199 Fix sentiment140 plugin 2018-06-18 17:43:10 +02:00
J. Fernando Sánchez
a1663a3f31 Upload latest with version 2018-06-18 17:36:30 +02:00
J. Fernando Sánchez
83b23dbdf4 UI improvements
* Add option to add multiple plugins
* Improve UI hints for collapsed parameters
* Refactored plugins without requirements
* Hide evaluation tab for the moment. You can see it by adding "?evaluation" to
  the URL.
2018-06-18 16:46:49 +02:00
29 changed files with 787 additions and 307 deletions

View File

@@ -76,8 +76,9 @@ pip_upload: pip_test ## Upload package to pip
push-latest: $(addprefix push-latest-,$(PYVERSIONS)) ## Push the "latest" tag to dockerhub
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGEWTAG)'
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGENAME)'
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGENAME):latest'
docker push '$(IMAGENAME):latest'
docker push '$(IMAGEWTAG)'
push-latest-%: build-% ## Push the latest image for a specific python version
docker tag $(IMAGENAME):$(VERSION)-python$* $(IMAGENAME):python$*

10
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,10 @@
version: '3'
services:
senpy:
image: "${IMAGENAME-gsiupm/senpy}:${VERSION-latest}"
entrypoint: ["/bin/bash"]
working_dir: "/senpy-plugins"
ports:
- 5000:5000
volumes:
- ".:/usr/src/app/"

9
docker-compose.test.yml Normal file
View File

@@ -0,0 +1,9 @@
version: '3'
services:
test:
image: "${IMAGENAME-gsiupm/senpy}:${VERSION-dev}"
entrypoint: ["py.test"]
volumes:
- ".:/usr/src/app/"
command:
[]

11
docker-compose.yml Normal file
View File

@@ -0,0 +1,11 @@
version: '3'
services:
senpy:
image: "${IMAGENAME-gsiupm/senpy}:${VERSION-dev}"
build:
context: .
dockerfile: Dockerfile${PYVERSION--2.7}
ports:
- 5001:5000
volumes:
- "./data:/data"

View File

@@ -43,7 +43,6 @@ class Dictionary(plugins.SentimentPlugin):
class EmojiOnly(Dictionary):
'''Sentiment annotation with a basic lexicon of emojis'''
description = 'A plugin'
dictionaries = [basic.emojis]
test_cases = [{

View File

@@ -12,3 +12,4 @@ rdflib-jsonld
numpy
scipy
scikit-learn
responses

View File

@@ -78,10 +78,15 @@ def main():
help='Do not run a server, only install plugin dependencies')
parser.add_argument(
'--only-test',
'-t',
action='store_true',
default=False,
help='Do not run a server, just test all plugins')
parser.add_argument(
'--test',
'-t',
action='store_true',
default=False,
help='Test all plugins before launching the server')
parser.add_argument(
'--only-list',
'--list',
@@ -99,12 +104,24 @@ def main():
action='store_false',
default=True,
help='Run a threaded server')
parser.add_argument(
'--no-deps',
'-n',
action='store_true',
default=False,
help='Skip installing dependencies')
parser.add_argument(
'--version',
'-v',
action='store_true',
default=False,
help='Output the senpy version and exit')
parser.add_argument(
'--allow-fail',
'--fail',
action='store_true',
default=False,
help='Do not exit if some plugins fail to activate')
args = parser.parse_args()
if args.version:
print('Senpy version {}'.format(senpy.__version__))
@@ -119,19 +136,27 @@ def main():
data_folder=args.data_folder)
if args.only_list:
plugins = sp.plugins()
maxwidth = max(len(x.id) for x in plugins)
maxname = max(len(x.name) for x in plugins)
maxversion = max(len(x.version) for x in plugins)
print('Found {} plugins:'.format(len(plugins)))
for plugin in plugins:
import inspect
fpath = inspect.getfile(plugin.__class__)
print('{: <{width}} @ {}'.format(plugin.id, fpath, width=maxwidth))
print('\t{: <{maxname}} @ {: <{maxversion}} -> {}'.format(plugin.name,
plugin.version,
fpath,
maxname=maxname,
maxversion=maxversion))
return
sp.install_deps()
if not args.no_deps:
sp.install_deps()
if args.only_install:
return
sp.activate_all()
if args.only_test:
sp.activate_all(allow_fail=args.allow_fail)
if args.test or args.only_test:
easy_test(sp.plugins(), debug=args.debug)
return
if args.only_test:
return
print('Senpy version {}'.format(senpy.__version__))
print('Server running on port %s:%d. Ctrl+C to quit' % (args.host,
args.port))

View File

@@ -3,6 +3,10 @@ from .models import Error, Results, Entry, from_string
import logging
logger = logging.getLogger(__name__)
boolean = [True, False]
API_PARAMS = {
"algorithm": {
"aliases": ["algorithms", "a", "algo"],
@@ -13,14 +17,14 @@ API_PARAMS = {
"expanded-jsonld": {
"@id": "expanded-jsonld",
"aliases": ["expanded"],
"options": "boolean",
"options": boolean,
"required": True,
"default": False
},
"with_parameters": {
"aliases": ['withparameters',
'with-parameters'],
"options": "boolean",
"options": boolean,
"default": False,
"required": True
},
@@ -29,14 +33,14 @@ API_PARAMS = {
"aliases": ["o"],
"default": "json-ld",
"required": True,
"options": ["json-ld", "turtle"],
"options": ["json-ld", "turtle", "ntriples"],
},
"help": {
"@id": "help",
"description": "Show additional help to know more about the possible parameters",
"aliases": ["h"],
"required": True,
"options": "boolean",
"options": boolean,
"default": False
},
"emotionModel": {
@@ -83,7 +87,7 @@ WEB_PARAMS = {
"aliases": ["headers"],
"required": True,
"default": False,
"options": "boolean"
"options": boolean
},
}
@@ -132,7 +136,7 @@ NIF_PARAMS = {
"aliases": ["u"],
"required": False,
"default": "RFC5147String",
"options": "RFC5147String"
"options": ["RFC5147String", ]
}
}
@@ -159,7 +163,7 @@ def parse_params(indict, *specs):
wrong_params[param] = spec[param]
continue
if "options" in options:
if options["options"] == "boolean":
if options["options"] == boolean:
outdict[param] = outdict[param] in [None, True, 'true', '1']
elif outdict[param] not in options["options"]:
wrong_params[param] = spec[param]
@@ -172,7 +176,7 @@ def parse_params(indict, *specs):
errors=wrong_params)
raise message
if 'algorithm' in outdict and not isinstance(outdict['algorithm'], list):
outdict['algorithm'] = outdict['algorithm'].split(',')
outdict['algorithm'] = list(outdict['algorithm'].split(','))
return outdict
@@ -190,7 +194,8 @@ def parse_call(params):
params = parse_params(params, NIF_PARAMS)
if params['informat'] == 'text':
results = Results()
entry = Entry(nif__isString=params['input'])
entry = Entry(nif__isString=params['input'],
id='#') # Use @base
results.entries.append(entry)
elif params['informat'] == 'json-ld':
results = from_string(params['input'], cls=Results)

View File

@@ -18,15 +18,15 @@
Blueprints for Senpy
"""
from flask import (Blueprint, request, current_app, render_template, url_for,
jsonify)
jsonify, redirect)
from .models import Error, Response, Help, Plugins, read_schema, dump_schema, Datasets
from . import api
from .version import __version__
from functools import wraps
import logging
import traceback
import json
import base64
logger = logging.getLogger(__name__)
@@ -34,6 +34,24 @@ api_blueprint = Blueprint("api", __name__)
demo_blueprint = Blueprint("demo", __name__, template_folder='templates')
ns_blueprint = Blueprint("ns", __name__)
_mimetypes_r = {'json-ld': ['application/ld+json'],
'turtle': ['text/turtle'],
'ntriples': ['application/n-triples'],
'text': ['text/plain']}
MIMETYPES = {}
for k, vs in _mimetypes_r.items():
for v in vs:
if v in MIMETYPES:
raise Exception('MIMETYPE {} specified for two formats: {} and {}'.format(v,
v,
MIMETYPES[v]))
MIMETYPES[v] = k
DEFAULT_MIMETYPE = 'application/ld+json'
DEFAULT_FORMAT = 'json-ld'
def get_params(req):
if req.method == 'POST':
@@ -45,22 +63,60 @@ def get_params(req):
return indict
def encoded_url(url=None, base=None):
code = ''
if not url:
if request.method == 'GET':
url = request.full_path[1:] # Remove the first slash
else:
hash(frozenset(request.form.params().items()))
code = 'hash:{}'.format(hash)
code = code or base64.urlsafe_b64encode(url.encode()).decode()
if base:
return base + code
return url_for('api.decode', code=code, _external=True)
def decoded_url(code, base=None):
if code.startswith('hash:'):
raise Exception('Can not decode a URL for a POST request')
base = base or request.url_root
path = base64.urlsafe_b64decode(code.encode()).decode()
return base + path
@demo_blueprint.route('/')
def index():
return render_template("index.html", version=__version__)
ev = str(get_params(request).get('evaluation', False))
evaluation_enabled = ev.lower() not in ['false', 'no', 'none']
return render_template("index.html",
evaluation=evaluation_enabled,
version=__version__)
@api_blueprint.route('/contexts/<entity>.jsonld')
def context(entity="context"):
context = Response._context
context['@vocab'] = url_for('ns.index', _external=True)
context['endpoint'] = url_for('api.api_root', _external=True)
return jsonify({"@context": context})
@api_blueprint.route('/d/<code>')
def decode(code):
try:
return redirect(decoded_url(code))
except Exception:
return Error('invalid URL').flask()
@ns_blueprint.route('/') # noqa: F811
def index():
context = Response._context
context['@vocab'] = url_for('.ns', _external=True)
context = Response._context.copy()
context['endpoint'] = url_for('api.api_root', _external=True)
return jsonify({"@context": context})
@@ -76,7 +132,7 @@ def basic_api(f):
default_params = {
'inHeaders': False,
'expanded-jsonld': False,
'outformat': 'json-ld',
'outformat': None,
'with_parameters': True,
}
@@ -95,42 +151,52 @@ def basic_api(f):
request.parameters = params
response = f(*args, **kwargs)
except (Exception) as ex:
if current_app.debug:
if current_app.debug or current_app.config['TESTING']:
raise
if not isinstance(ex, Error):
msg = "{}:\n\t{}".format(ex,
traceback.format_exc())
msg = "{}".format(ex)
ex = Error(message=msg, status=500)
logger.exception('Error returning analysis result')
response = ex
response.parameters = raw_params
logger.error(ex)
logger.exception(ex)
if 'parameters' in response and not params['with_parameters']:
del response.parameters
logger.info('Response: {}'.format(response))
mime = request.accept_mimetypes\
.best_match(MIMETYPES.keys(),
DEFAULT_MIMETYPE)
mimeformat = MIMETYPES.get(mime, DEFAULT_FORMAT)
outformat = params['outformat'] or mimeformat
return response.flask(
in_headers=params['inHeaders'],
headers=headers,
prefix=url_for('.api_root', _external=True),
prefix=params.get('prefix', encoded_url()),
context_uri=url_for('api.context',
entity=type(response).__name__,
_external=True),
outformat=params['outformat'],
outformat=outformat,
expanded=params['expanded-jsonld'])
return decorated_function
@api_blueprint.route('/', methods=['POST', 'GET'])
@api_blueprint.route('/', defaults={'plugin': None}, methods=['POST', 'GET'])
@api_blueprint.route('/<path:plugin>', methods=['POST', 'GET'])
@basic_api
def api_root():
def api_root(plugin):
if request.parameters['help']:
dic = dict(api.API_PARAMS, **api.NIF_PARAMS)
response = Help(valid_parameters=dic)
return response
req = api.parse_call(request.parameters)
if plugin:
plugin = plugin.replace('+', '/')
plugin = plugin.split('/')
req.parameters['algorithm'] = plugin
return current_app.senpy.analyse(req)
@@ -160,7 +226,7 @@ def plugins():
@api_blueprint.route('/plugins/<plugin>/', methods=['POST', 'GET'])
@basic_api
def plugin(plugin=None):
def plugin(plugin):
sp = current_app.senpy
return sp.get_plugin(plugin)

View File

@@ -13,7 +13,7 @@ class Client(object):
return self.request('/', method=method, input=input, **kwargs)
def evaluate(self, input, method='GET', **kwargs):
return self.request('/evaluate', method = method, input=input, **kwargs)
return self.request('/evaluate', method=method, input=input, **kwargs)
def plugins(self, *args, **kwargs):
resp = self.request(path='/plugins').plugins
@@ -24,7 +24,7 @@ class Client(object):
return {d.name: d for d in resp}
def request(self, path=None, method='GET', **params):
url = '{}{}'.format(self.endpoint, path)
url = '{}{}'.format(self.endpoint.rstrip('/'), path)
response = requests.request(method=method, url=url, params=params)
try:
resp = models.from_dict(response.json())

View File

@@ -18,14 +18,9 @@ import errno
import logging
logger = logging.getLogger(__name__)
from . import gsitk_compat
try:
from gsitk.datasets.datasets import DatasetManager
GSITK_AVAILABLE = True
except ImportError:
logger.warn('GSITK is not installed. Some functions will be unavailable.')
GSITK_AVAILABLE = False
logger = logging.getLogger(__name__)
class Senpy(object):
@@ -95,7 +90,7 @@ class Senpy(object):
if plugin in self._plugins:
return self._plugins[plugin]
results = self.plugins(id='plugins/{}'.format(name))
results = self.plugins(id='endpoint:plugins/{}'.format(name))
if not results:
return Error(message="Plugin not found", status=404)
@@ -158,7 +153,6 @@ class Senpy(object):
yield i
return
plugin = plugins[0]
self._activate(plugin) # Make sure the plugin is activated
specific_params = api.parse_extra_params(req, plugin)
req.analysis.append({'plugin': plugin,
'parameters': specific_params})
@@ -167,8 +161,7 @@ class Senpy(object):
yield i
def install_deps(self):
for plugin in self.plugins(is_activated=True):
plugins.install_deps(plugin)
plugins.install_deps(*self.plugins())
def analyse(self, request):
"""
@@ -203,16 +196,14 @@ class Senpy(object):
raise Error(
status=404,
message="The dataset '{}' is not valid".format(dataset))
dm = DatasetManager()
dm = gsitk_compat.DatasetManager()
datasets = dm.prepare_datasets(datasets_name)
return datasets
@property
def datasets(self):
if not GSITK_AVAILABLE:
raise Exception('GSITK is not available. Install it to use this function.')
self._dataset_list = {}
dm = DatasetManager()
dm = gsitk_compat.DatasetManager()
for item in dm.get_datasets():
for key in item:
if key in self._dataset_list:
@@ -223,8 +214,6 @@ class Senpy(object):
return self._dataset_list
def evaluate(self, params):
if not GSITK_AVAILABLE:
raise Exception('GSITK is not available. Install it to use this function.')
logger.debug("evaluating request: {}".format(params))
results = AggregatedEvaluation()
results.parameters = params
@@ -318,10 +307,15 @@ class Senpy(object):
else:
self._default = self._plugins[value.lower()]
def activate_all(self, sync=True):
def activate_all(self, sync=True, allow_fail=False):
ps = []
for plug in self._plugins.keys():
ps.append(self.activate_plugin(plug, sync=sync))
try:
self.activate_plugin(plug, sync=sync)
except Exception as ex:
if not allow_fail:
raise
logger.error('Could not activate {}: {}'.format(plug, ex))
return ps
def deactivate_all(self, sync=True):
@@ -346,6 +340,7 @@ class Senpy(object):
logger.info(msg)
success = True
self._set_active(plugin, success)
return success
def activate_plugin(self, plugin_name, sync=True):
plugin_name = plugin_name.lower()
@@ -356,8 +351,8 @@ class Senpy(object):
logger.info("Activating plugin: {}".format(plugin.name))
if sync or 'async' in plugin and not plugin.async:
self._activate(plugin)
if sync or not getattr(plugin, 'async', True):
return self._activate(plugin)
else:
th = Thread(target=partial(self._activate, plugin))
th.start()
@@ -379,7 +374,7 @@ class Senpy(object):
self._set_active(plugin, False)
if sync or 'async' in plugin and not plugin.async:
if sync or not getattr(plugin, 'async', True):
self._deactivate(plugin)
else:
th = Thread(target=partial(self._deactivate, plugin))

23
senpy/gsitk_compat.py Normal file
View File

@@ -0,0 +1,23 @@
import logging
logger = logging.getLogger(__name__)
MSG = 'GSITK is not (properly) installed.'
IMPORTMSG = '{} Some functions will be unavailable.'.format(MSG)
RUNMSG = '{} Install it to use this function.'.format(MSG)
def raise_exception(*args, **kwargs):
raise Exception(RUNMSG)
try:
from gsitk.datasets.datasets import DatasetManager
from gsitk.evaluation.evaluation import Evaluation as Eval
from sklearn.pipeline import Pipeline
GSITK_AVAILABLE = True
modules = locals()
except ImportError:
logger.warn(IMPORTMSG)
GSITK_AVAILABLE = False
DatasetManager = Eval = Pipeline = raise_exception

View File

@@ -138,7 +138,7 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
@property
def id(self):
if '@id' not in self:
self['@id'] = ':{}_{}'.format(type(self).__name__, time.time())
self['@id'] = '_:{}_{}'.format(type(self).__name__, time.time())
return self['@id']
@id.setter
@@ -146,7 +146,7 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
self['@id'] = value
def flask(self,
in_headers=True,
in_headers=False,
headers=None,
outformat='json-ld',
**kwargs):
@@ -176,20 +176,22 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
def serialize(self, format='json-ld', with_mime=False, **kwargs):
js = self.jsonld(**kwargs)
content = json.dumps(js, indent=2, sort_keys=True)
if format == 'json-ld':
content = json.dumps(js, indent=2, sort_keys=True)
mimetype = "application/json"
elif format in ['turtle', ]:
elif format in ['turtle', 'ntriples']:
logger.debug(js)
content = json.dumps(js, indent=2, sort_keys=True)
base = kwargs.get('prefix')
g = Graph().parse(
data=content,
format='json-ld',
base=kwargs.get('prefix'),
context=self._context)
base=base,
context=[self._context,
{'@base': base}])
logger.debug(
'Parsing with prefix: {}'.format(kwargs.get('prefix')))
content = g.serialize(format='turtle').decode('utf-8')
content = g.serialize(format=format,
base=base).decode('utf-8')
mimetype = 'text/{}'.format(format)
else:
raise Error('Unknown outformat: {}'.format(format))
@@ -205,25 +207,21 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
expanded=False):
result = self.serializable()
if context_uri or with_context:
result['@context'] = context_uri or self._context
# result = jsonld.compact(result,
# self._context,
# options={
# 'base': prefix,
# 'expandContext': self._context,
# 'senpy': prefix
# })
if expanded:
result = jsonld.expand(
result, options={'base': prefix,
'expandContext': self._context})
'expandContext': self._context})[0]
if not with_context:
try:
del result['@context']
except KeyError:
pass
elif context_uri:
result['@context'] = context_uri
else:
result['@context'] = self._context
return result
def validate(self, obj=None):

View File

@@ -3,6 +3,7 @@ standard_library.install_aliases()
from future.utils import with_metaclass
from functools import partial
import os.path
import os
@@ -22,18 +23,12 @@ import nltk
from .. import models, utils
from .. import api
from .. import gsitk_compat
from .. import testing
logger = logging.getLogger(__name__)
try:
from gsitk.evaluation.evaluation import Evaluation as Eval
from sklearn.pipeline import Pipeline
GSITK_AVAILABLE = True
except ImportError:
logger.warn('GSITK is not installed. Some functions will be unavailable.')
GSITK_AVAILABLE = False
class PluginMeta(models.BaseMeta):
_classes = {}
@@ -92,7 +87,7 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
if info:
self.update(info)
self.validate()
self.id = 'plugins/{}_{}'.format(self['name'], self['version'])
self.id = 'endpoint:plugins/{}_{}'.format(self['name'], self['version'])
self.is_activated = False
self._lock = threading.Lock()
self._directory = os.path.abspath(os.path.dirname(inspect.getfile(self.__class__)))
@@ -149,14 +144,23 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
self.log.warn('Test case failed:\n{}'.format(pprint.pformat(case)))
raise
def test_case(self, case):
def test_case(self, case, mock=testing.MOCK_REQUESTS):
entry = models.Entry(case['entry'])
given_parameters = case.get('params', case.get('parameters', {}))
expected = case.get('expected', None)
should_fail = case.get('should_fail', False)
responses = case.get('responses', [])
try:
params = api.parse_params(given_parameters, self.extra_params)
res = list(self.analyse_entries([entry, ], params))
method = partial(self.analyse_entries, [entry, ], params)
if mock:
res = list(method())
else:
with testing.patch_all_requests(responses):
res = list(method())
if not isinstance(expected, list):
expected = [expected]
@@ -332,7 +336,7 @@ class Box(AnalysisPlugin):
return self.transform(X)
def as_pipe(self):
pipe = Pipeline([('plugin', self)])
pipe = gsitk_compat.Pipeline([('plugin', self)])
pipe.name = self.name
return pipe
@@ -509,7 +513,7 @@ def install_deps(*plugins):
exitcode = process.wait()
installed = True
if exitcode != 0:
raise models.Error("Dependencies not properly installed")
raise models.Error("Dependencies not properly installed: {}".format(pip_args))
nltk_resources |= set(info.get('nltk_resources', []))
installed |= nltk.download(list(nltk_resources))
@@ -530,7 +534,7 @@ def find_plugins(folders):
yield fpath
def from_path(fpath, **kwargs):
def from_path(fpath, install_on_fail=False, **kwargs):
logger.debug("Loading plugin from {}".format(fpath))
if fpath.endswith('.py'):
# We asume root is the dir of the file, and module is the name of the file
@@ -540,7 +544,7 @@ def from_path(fpath, **kwargs):
yield instance
else:
info = parse_plugin_info(fpath)
yield from_info(info, **kwargs)
yield from_info(info, install_on_fail=install_on_fail, **kwargs)
def from_folder(folders, loader=from_path, **kwargs):
@@ -551,7 +555,7 @@ def from_folder(folders, loader=from_path, **kwargs):
return plugins
def from_info(info, root=None, **kwargs):
def from_info(info, root=None, install_on_fail=True, **kwargs):
if any(x not in info for x in ('module',)):
raise ValueError('Plugin info is not valid: {}'.format(info))
module = info["module"]
@@ -559,7 +563,12 @@ def from_info(info, root=None, **kwargs):
if not root and '_path' in info:
root = os.path.dirname(info['_path'])
return one_from_module(module, root=root, info=info, **kwargs)
fun = partial(one_from_module, module, root=root, info=info, **kwargs)
try:
return fun()
except (ImportError, LookupError):
install_deps(info)
return fun()
def parse_plugin_info(fpath):
@@ -606,17 +615,9 @@ def _instances_in_module(module):
yield obj
def _from_module_name(module, root, info=None, install=True, **kwargs):
try:
module = load_module(module, root)
except (ImportError, LookupError):
if not install or not info:
raise
install_deps(info)
module = load_module(module, root)
def _from_module_name(module, root, info=None, **kwargs):
module = load_module(module, root)
for plugin in _from_loaded_module(module=module, root=root, info=info, **kwargs):
if install:
install_deps(plugin)
yield plugin
@@ -628,12 +629,9 @@ def _from_loaded_module(module, info=None, **kwargs):
def evaluate(plugins, datasets, **kwargs):
if not GSITK_AVAILABLE:
raise Exception('GSITK is not available. Install it to use this function.')
ev = Eval(tuples=None,
datasets=datasets,
pipelines=[plugin.as_pipe() for plugin in plugins])
ev = gsitk_compat.Eval(tuples=None,
datasets=datasets,
pipelines=[plugin.as_pipe() for plugin in plugins])
ev.evaluate()
results = ev.results
evaluations = evaluations_to_JSONLD(results, **kwargs)

View File

@@ -0,0 +1,187 @@
from senpy import AnalysisPlugin, easy
class maxSentiment(AnalysisPlugin):
'''Plugin to extract max emotion from a multi-empotion set'''
author = '@dsuarezsouto'
version = '0.1'
extra_params = {
'max': {
"aliases": ["maximum", "max"],
'required': True,
'options': [True, False],
"@id": 'maxSentiment',
'default': False
}
}
def analyse_entry(self, entry, params):
if not params["max"]:
yield entry
return
set_emotions= entry.emotions[0]['onyx:hasEmotion']
max_emotion =set_emotions[0]
# Extract max emotion from the set emotions (emotion with highest intensity)
for tmp_emotion in set_emotions:
if tmp_emotion['onyx:hasEmotionIntensity']>max_emotion['onyx:hasEmotionIntensity']:
max_emotion=tmp_emotion
if max_emotion['onyx:hasEmotionIntensity'] == 0:
max_emotion['onyx:hasEmotionCategory'] = "neutral"
max_emotion['onyx:hasEmotionIntensity'] = 1.0
entry.emotions[0]['onyx:hasEmotion'] = [max_emotion]
entry.emotions[0]['prov:wasGeneratedBy'] = "maxSentiment"
#print(entry)
yield entry
# Test Cases:
# 1 Normal Situation.
# 2 Case to return a Neutral Emotion.
test_cases = [{
"entry": {
"@type": "entry",
"emotions": [
{
"@id": "Emotions0",
"@type": "emotionSet",
"onyx:hasEmotion": [
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "anger",
"onyx:hasEmotionIntensity": 0
},
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "joy",
"onyx:hasEmotionIntensity": 0.3333333333333333
},
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "negative-fear",
"onyx:hasEmotionIntensity": 0
},
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "sadness",
"onyx:hasEmotionIntensity": 0
},
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "disgust",
"onyx:hasEmotionIntensity": 0
}
]
},
],
"nif:isString": "This text makes me sad.\nwhilst this text makes me happy and surprised at the same time.\nI cannot believe it!"
},
'params': {
'max': True
},
'expected' : {
"@type": "entry",
"emotions": [
{
"@id": "Emotions0",
"@type": "emotionSet",
"onyx:hasEmotion": [
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "joy",
"onyx:hasEmotionIntensity": 0.3333333333333333
}
],
"prov:wasGeneratedBy" : 'maxSentiment'
}
],
"nif:isString": "This text makes me sad.\nwhilst this text makes me happy and surprised at the same time.\nI cannot believe it!"
}
},
{
"entry": {
"@type": "entry",
"emotions": [
{
"@id": "Emotions0",
"@type": "emotionSet",
"onyx:hasEmotion": [
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "anger",
"onyx:hasEmotionIntensity": 0
},
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "joy",
"onyx:hasEmotionIntensity": 0
},
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "negative-fear",
"onyx:hasEmotionIntensity": 0
},
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "sadness",
"onyx:hasEmotionIntensity": 0
},
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "disgust",
"onyx:hasEmotionIntensity": 0
}
]
}
],
"nif:isString": "This text makes me sad.\nwhilst this text makes me happy and surprised at the same time.\nI cannot believe it!"
},
'params': {
'max': True
},
'expected' : {
"@type": "entry",
"emotions": [
{
"@id": "Emotions0",
"@type": "emotionSet",
"onyx:hasEmotion": [
{
"@id": "_:Emotion_1538121033.74",
"@type": "emotion",
"onyx:hasEmotionCategory": "neutral",
"onyx:hasEmotionIntensity": 1
}
],
"prov:wasGeneratedBy" : 'maxSentiment'
}
],
"nif:isString": "This text makes me sad.\nwhilst this text makes me happy and surprised at the same time.\nI cannot believe it!"
}
}]
if __name__ == '__main__':
easy()

View File

@@ -1,19 +0,0 @@
---
name: split
module: senpy.plugins.misc.split
description: A sample plugin that chunks input text
author: "@militarpancho"
version: '0.2'
url: "https://github.com/gsi-upm/senpy"
requirements:
- nltk
extra_params:
delimiter:
aliases:
- type
- t
required: false
default: sentence
options:
- sentence
- paragraph

View File

@@ -5,13 +5,27 @@ from nltk.tokenize.simple import LineTokenizer
import nltk
class SplitPlugin(AnalysisPlugin):
class Split(AnalysisPlugin):
'''description: A sample plugin that chunks input text'''
author = ["@militarpancho", '@balkian']
version = '0.2'
url = "https://github.com/gsi-upm/senpy"
extra_params = {
'delimiter': {
'aliases': ['type', 't'],
'required': False,
'default': 'sentence',
'options': ['sentence', 'paragraph']
},
}
def activate(self):
nltk.download('punkt')
def analyse_entry(self, entry, params):
yield entry
chunker_type = params["delimiter"]
original_text = entry['nif:isString']
if chunker_type == "sentence":

View File

@@ -1,22 +0,0 @@
---
name: sentiment140
module: sentiment140
description: "Connects to the sentiment140 free API: http://sentiment140.com"
author: "@balkian"
version: '0.2'
url: "https://github.com/gsi-upm/senpy-plugins-community"
extra_params:
language:
"@id": lang_sentiment140
aliases:
- language
- l
required: false
options:
- es
- en
- auto
default: auto
requirements: {}
maxPolarityValue: 1
minPolarityValue: 0

View File

@@ -4,12 +4,31 @@ import json
from senpy.plugins import SentimentPlugin
from senpy.models import Sentiment
ENDPOINT = 'http://www.sentiment140.com/api/bulkClassifyJson'
class Sentiment140Plugin(SentimentPlugin):
class Sentiment140(SentimentPlugin):
'''Connects to the sentiment140 free API: http://sentiment140.com'''
author = "@balkian"
version = '0.2'
url = "https://github.com/gsi-upm/senpy-plugins-community"
extra_params = {
'language': {
"@id": 'lang_sentiment140',
'aliases': ['language', 'l'],
'required': False,
'default': 'auto',
'options': ['es', 'en', 'auto']
}
}
maxPolarityValue = 1
minPolarityValue = 0
def analyse_entry(self, entry, params):
lang = params["language"]
res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson",
res = requests.post(ENDPOINT,
json.dumps({
"language": lang,
"data": [{
@@ -31,23 +50,10 @@ class Sentiment140Plugin(SentimentPlugin):
marl__hasPolarity=polarity,
marl__polarityValue=polarity_value)
sentiment.prov__wasGeneratedBy = self.id
entry.sentiments = []
entry.sentiments.append(sentiment)
entry.language = lang
yield entry
def test(self, *args, **kwargs):
'''
To avoid calling the sentiment140 API, we will mock the results
from requests.
'''
from senpy.testing import patch_requests
expected = {"data": [{"polarity": 4}]}
with patch_requests(expected) as (request, response):
super(Sentiment140Plugin, self).test(*args, **kwargs)
assert request.called
assert response.json.called
test_cases = [
{
'entry': {
@@ -61,6 +67,9 @@ class Sentiment140Plugin(SentimentPlugin):
'marl:hasPolarity': 'marl:Positive',
}
]
}
},
'responses': [{'url': ENDPOINT,
'method': 'POST',
'json': {'data': [{'polarity': 4}]}}]
}
]

View File

@@ -167,3 +167,36 @@ textarea{
color: inherit;
text-decoration: inherit;
}
.collapsed .collapseicon {
display: none !important;
}
.collapsed .expandicon {
display: inline-block !important;
}
.expandicon {
display: none !important;
}
.collapseicon {
display: inline-block !important;
}
.loader {
border: 6px solid #f3f3f3; /* Light grey */
border-top: 6px solid blue;
border-bottom: 6px solid blue;
border-radius: 50%;
width: 3em;
height: 3em;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View File

@@ -4,6 +4,7 @@ var plugins_params = default_params = {};
var plugins = [];
var defaultPlugin = {};
var gplugins = {};
var pipeline = [];
function replaceURLWithHTMLLinks(text) {
console.log('Text: ' + text);
@@ -30,7 +31,10 @@ function hashchanged(){
function get_plugins(response){
plugins = response.plugins;
for(ix in response.plugins){
plug = response.plugins[ix];
plugins[plug.name] = plug;
}
}
function get_datasets(response){
@@ -83,10 +87,32 @@ function draw_plugins_selection(){
html += "</optgroup>"
// Two elements with plugin class
// One from the evaluate tab and another one from the analyse tab
document.getElementsByClassName('plugin')[0].innerHTML = html;
document.getElementsByClassName('plugin')[1].innerHTML = html;
plugin_lists = document.getElementsByClassName('plugin')
for (element in plugin_lists){
plugin_lists[element].innerHTML = html;
}
draw_plugin_pipeline();
}
function draw_plugin_pipeline(){
var pipeHTML = "";
console.log("Drawing pipeline: ", pipeline);
for (ix in pipeline){
plug = pipeline[ix];
pipeHTML += '<span onclick="remove_plugin_pipeline(\'' + plug + '\')" class="btn btn-primary"><span ><i class="fa fa-minus"></i></span> ' + plug + '</span> <i class="fa fa-arrow-right"></i> ';
}
console.log(pipeHTML);
$("#pipeline").html(pipeHTML);
}
function remove_plugin_pipeline(name){
console.log("Removing plugin: ", name);
var index = pipeline.indexOf(name);
pipeline.splice(index, 1);
draw_plugin_pipeline();
}
function draw_plugins_list(){
var availablePlugins = document.getElementById('availablePlugins');
@@ -105,6 +131,13 @@ function draw_plugins_list(){
}
}
function add_plugin_pipeline(){
var selected = get_selected_plugin();
pipeline.push(selected);
console.log("Adding ", selected);
draw_plugin_pipeline();
}
function draw_datasets(){
html = "";
repeated_html = "<input class=\"checks-datasets\" type=\"checkbox\" value=\"";
@@ -118,16 +151,20 @@ function draw_datasets(){
$(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);
var response2 = JSON.parse($.ajax({type: "GET", url: "/api/datasets/" , async: false}).responseText);
get_plugins(response);
get_default_parameters();
get_datasets(response2);
draw_plugins_list();
draw_plugins_selection();
draw_parameters();
draw_datasets();
draw_plugin_description();
if (evaluation_enabled) {
var response2 = JSON.parse($.ajax({type: "GET", url: "/api/datasets/" , async: false}).responseText);
get_datasets(response2);
draw_datasets();
}
$(window).on('hashchange', hashchanged);
hashchanged();
@@ -144,17 +181,34 @@ function get_default_parameters(){
}
function get_selected_plugin(){
return document.getElementsByClassName('plugin')[0].options[document.getElementsByClassName('plugin')[0].selectedIndex].value;
}
function draw_default_parameters(){
var basic_params = document.getElementById("basic_params");
basic_params.innerHTML = params_div(default_params);
}
function update_params(params, plug){
ep = plugins_params[plug];
for(k in ep){
params[k] = ep[k];
}
return params
}
function draw_extra_parameters(){
var plugin = document.getElementsByClassName('plugin')[0].options[document.getElementsByClassName('plugin')[0].selectedIndex].value;
var plugin = get_selected_plugin();
get_parameters();
var extra_params = document.getElementById("extra_params");
extra_params.innerHTML = params_div(plugins_params[plugin]);
var params = {};
for (sel in pipeline){
update_params(params, pipeline[sel]);
}
update_params(params, plugin);
extra_params.innerHTML = params_div(params);
}
function draw_parameters(){
@@ -261,6 +315,15 @@ function add_param(key, value){
return "&"+key+"="+value;
}
function get_pipeline_arg(){
arg = "";
for (ix in pipeline){
arg = arg + pipeline[ix] + ",";
}
arg = arg + get_selected_plugin();
return arg;
}
function load_JSON(){
url = "/api";
@@ -269,7 +332,9 @@ function load_JSON(){
rawcontainer.innerHTML = '';
container.innerHTML = '';
var plugin = document.getElementsByClassName("plugin")[0].options[document.getElementsByClassName("plugin")[0].selectedIndex].value;
var plugin = get_pipeline_arg();
$(".loading").addClass("loader");
$("#preview").hide();
var input = encodeURIComponent(document.getElementById("input").value);
url += "?algo="+plugin+"&i="+input
@@ -280,27 +345,27 @@ function load_JSON(){
url += add_param(key, params[key]);
}
var response = $.ajax({type: "GET", url: url , async: false}).responseText;
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
$.ajax({type: "GET", url: url}).always(function(response){
document.getElementById("results-div").style.display = 'block';
if(typeof response=="object") {
var options = {
mode: 'view'
};
var editor = new JSONEditor(container, options, response);
editor.expandAll();
$('#results-div a[href="#viewer"]').click();
response = JSON.stringify(response, null, 4);
} else {
console.log("Got turtle?");
$('#results-div a[href="#raw"]').click();
}
document.getElementById("input_request").innerHTML = "<a href='"+url+"'>"+url+"</a>"
document.getElementById("results-div").style.display = 'block';
try {
response = JSON.parse(response);
var options = {
mode: 'view'
};
var editor = new JSONEditor(container, options, response);
editor.expandAll();
// $('#results-div a[href="#viewer"]').tab('show');
$('#results-div a[href="#viewer"]').click();
// location.hash = 'raw';
}
catch(err){
console.log("Error decoding JSON (got turtle?)");
$('#results-div a[href="#raw"]').click();
// location.hash = 'raw';
}
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
document.getElementById("input_request").innerHTML = "<a href='"+url+"'>"+url+"</a>"
$(".loading").removeClass("loader");
$("#preview").show();
});
}
function get_datasets_from_checkbox(){
@@ -347,40 +412,53 @@ function evaluate_JSON(){
url += "?algo="+plugin+"&dataset="+datasets
var response = $.ajax({type: "GET", url: url , async: false, dataType: 'json'}).responseText;
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
$('#doevaluate').attr("disabled", true);
$.ajax({type: "GET", url: url, dataType: 'json'}).always(function(resp) {
$('#doevaluate').attr("disabled", false);
response = resp.responseText;
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
document.getElementById("input_request_eval").innerHTML = "<a href='"+url+"'>"+url+"</a>"
document.getElementById("evaluate-div").style.display = 'block';
try {
response = JSON.parse(response);
var options = {
mode: 'view'
};
//Control the single response results
if (!(Array.isArray(response.evaluations))){
response.evaluations = [response.evaluations]
}
new_tbody = create_body_metrics(response.evaluations)
table.replaceChild(new_tbody, table.lastElementChild)
var editor = new JSONEditor(container, options, response);
editor.expandAll();
// $('#results-div a[href="#viewer"]').tab('show');
$('#evaluate-div a[href="#evaluate-table"]').click();
// location.hash = 'raw';
document.getElementById("input_request_eval").innerHTML = "<a href='"+url+"'>"+url+"</a>"
document.getElementById("evaluate-div").style.display = 'block';
try {
response = JSON.parse(response);
var options = {
mode: 'view'
};
//Control the single response results
if (!(Array.isArray(response.evaluations))){
response.evaluations = [response.evaluations]
}
catch(err){
console.log("Error decoding JSON (got turtle?)");
$('#evaluate-div a[href="#evaluate-raw"]').click();
// location.hash = 'raw';
}
})
}
new_tbody = create_body_metrics(response.evaluations)
table.replaceChild(new_tbody, table.lastElementChild)
function draw_plugin_description(){
var plugin = plugins[get_selected_plugin()];
$("#plugdescription").text(plugin.description);
console.log(plugin);
}
var editor = new JSONEditor(container, options, response);
editor.expandAll();
// $('#results-div a[href="#viewer"]').tab('show');
$('#evaluate-div a[href="#evaluate-table"]').click();
// location.hash = 'raw';
}
catch(err){
console.log("Error decoding JSON (got turtle?)");
$('#evaluate-div a[href="#evaluate-raw"]').click();
// location.hash = 'raw';
}
}
function plugin_selected(){
draw_extra_parameters();
draw_plugin_description();
}

View File

@@ -5,6 +5,9 @@
<title>Playground {{version}}</title>
</head>
<script>
this.evaluation_enabled = {% if evaluation %}true{%else %}false{%endif%};
</script>
<script src="static/js/jquery-2.1.1.min.js" ></script>
<!--<script src="jquery.autosize.min.js"></script>-->
<link rel="stylesheet" href="static/css/bootstrap.min.css">
@@ -32,7 +35,9 @@
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" ><a class="active" href="#about">About</a></li>
<li role="presentation"class="active"><a class="active" href="#test">Test it</a></li>
{% if evaluation %}
<li role="presentation"><a class="active" href="#evaluate">Evaluate Plugins</a></li>
{% endif %}
</ul>
@@ -61,6 +66,14 @@
</ul>
</p>
<p>Senpy is a research project. If you use it in your research, please cite:
<pre>
Senpy: A Pragmatic Linked Sentiment Analysis Framework.
Sánchez-Rada, J. F., Iglesias, C. A., Corcuera, I., & Araque, Ó.
In Data Science and Advanced Analytics (DSAA),
2016 IEEE International Conference on (pp. 735-742). IEEE.
</pre>
</p>
</div>
<div class="col-lg-6 ">
@@ -70,8 +83,6 @@
</div>
<div class="panel-body"><ul id=availablePlugins></ul></div>
</div>
</div>
<div class="col-lg-6 ">
<a href="http://senpy.readthedocs.io">
<div class="panel panel-default">
<div class="panel-heading"><i class="fa fa-book"></i> If you are new to senpy, you might want to read senpy's documentation</div>
@@ -82,9 +93,6 @@
<div class="panel-heading"><i class="fa fa-sign-in"></i> Feel free to follow us on GitHub</div>
</div>
</a>
<div class="panel panel-default">
<div class="panel-heading"><i class="fa fa-child"></i> Enjoy.</div>
</div>
</div>
</div>
</div>
@@ -96,17 +104,28 @@
whilst this text makes me happy and surprised at the same time.
I cannot believe it!</textarea>
</div>
<div>
<label>Select the plugin:</label>
<select id="plugins" name="plugins" class=plugin 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">
Select the plugin.
</h4>
</div>
<div id="plugin_selection" class="panel-collapse panel-body">
<span id="pipeline"></span>
<select name="plugins" class="plugin" onchange="plugin_selected()">
</select>
<span onclick="add_plugin_pipeline()"><span class="btn"><i class="fa fa-plus" title="Add more plugins to the pipeline. Processing order is left to right. i.e. the results of the leftmost plugin will be used as input for the second leftmost, and so on."></i></span></span>
<label class="help-block " id="plugdescription"></label>
</div>
</div>
<div class="panel panel-default">
<a data-toggle="collapse" class="deco-none collapsed" href="#basic_params">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fa fa-chevron-right pull-left expandicon"></i>
<i class="fa fa-chevron-down pull-left collapseicon"></i>
Basic API parameters
</h4>
</div>
@@ -118,6 +137,8 @@ I cannot believe it!</textarea>
<a data-toggle="collapse" class="deco-none" href="#extra_params">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fa fa-chevron-right pull-left expandicon"></i>
<i class="fa fa-chevron-down pull-left collapseicon"></i>
Plugin extra parameters
</h4>
</div>
@@ -129,6 +150,7 @@ I cannot believe it!</textarea>
<!-- END PARAMETERS -->
<a id="preview" class="btn btn-lg btn-primary" onclick="load_JSON()">Analyse!</a>
<div id="loading-results" class="loading"></div>
<!--<button id="visualise" name="type" type="button">Visualise!</button>-->
</form>
</div>
@@ -155,6 +177,8 @@ I cannot believe it!</textarea>
</div>
</div>
{% if evaluation %}
<div class="tab-pane" id="evaluate">
<div class="well">
<form id="form" class="container" onsubmit="return getPlugins();" accept-charset="utf-8">
@@ -169,7 +193,7 @@ I cannot believe it!</textarea>
</select>
</div>
<a id="preview" class="btn btn-lg btn-primary" onclick="evaluate_JSON()">Evaluate Plugin!</a>
<a id="doevaluate" class="btn btn-lg btn-primary" onclick="evaluate_JSON()">Evaluate Plugin!</a>
<!--<button id="visualise" name="type" type="button">Visualise!</button>-->
</form>
</div>
@@ -216,6 +240,7 @@ I cannot believe it!</textarea>
</div>
</div>
</div>
{% endif %}
</div>
<a href="http://www.gsi.dit.upm.es" target="_blank"><img class="center-block" src="static/img/gsi.png"/> </a>

View File

@@ -1,36 +1,31 @@
try:
from unittest.mock import patch, MagicMock
except ImportError:
from mock import patch, MagicMock
from past.builtins import basestring
import json
from contextlib import contextmanager
import os
import responses as requestmock
from .models import BaseModel
@contextmanager
def patch_requests(value, code=200):
success = MagicMock()
if isinstance(value, BaseModel):
value = value.jsonld()
if not isinstance(value, basestring):
data = json.dumps(value)
MOCK_REQUESTS = os.environ.get('MOCK_REQUESTS', '').lower() in ['no', 'false']
def patch_all_requests(responses):
patched = requestmock.RequestsMock()
for response in responses or []:
args = response.copy()
if 'json' in args and isinstance(args['json'], BaseModel):
args['json'] = args['json'].jsonld()
args['method'] = getattr(requestmock, args.get('method', 'GET'))
patched.add(**args)
return patched
def patch_requests(url, response, method='GET', status=200):
args = {'url': url, 'method': method, 'status': status}
if isinstance(response, basestring):
args['body'] = response
else:
data = value
success.json.return_value = value
success.status_code = code
success.content = data
success.text = data
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
args['json'] = response
return patch_all_requests([args])

View File

@@ -80,7 +80,7 @@ def easy_test(plugin_list=None, debug=True):
for plug in plugin_list:
plug.test()
plug.log.info('My tests passed!')
logger.info('All tests passed!')
logger.info('All tests passed for {} plugins!'.format(len(plugin_list)))
except Exception:
if not debug:
raise

View File

@@ -21,7 +21,6 @@ class BlueprintsTest(TestCase):
def setUpClass(cls):
"""Set up only once, and re-use in every individual test"""
cls.app = Flask("test_extensions")
cls.app.debug = False
cls.client = cls.app.test_client()
cls.senpy = Senpy(default_plugins=True)
cls.senpy.init_app(cls.app)
@@ -31,17 +30,21 @@ class BlueprintsTest(TestCase):
cls.senpy.activate_plugin("DummyRequired", sync=True)
cls.senpy.default_plugin = 'Dummy'
def setUp(self):
self.app.config['TESTING'] = True # Tell Flask not to catch Exceptions
def assertCode(self, resp, code):
self.assertEqual(resp.status_code, code)
def test_playground(self):
resp = self.client.get("/")
assert "main.js" in resp.data.decode()
assert "main.js" in resp.get_data(as_text=True)
def test_home(self):
"""
Calling with no arguments should ask the user for more arguments
"""
self.app.config['TESTING'] = False # Errors are expected in this case
resp = self.client.get("/api/")
self.assertCode(resp, 400)
js = parse_resp(resp)
@@ -67,7 +70,7 @@ class BlueprintsTest(TestCase):
def test_analysis_extra(self):
"""
Extra params that have a default should
Extra params that have a default should use it
"""
resp = self.client.get("/api/?i=My aloha mohame&algo=Dummy&with_parameters=true")
self.assertCode(resp, 200)
@@ -81,7 +84,7 @@ class BlueprintsTest(TestCase):
Extra params that have a required argument that does not
have a default should raise an error.
"""
self.app.debug = False
self.app.config['TESTING'] = False # Errors are expected in this case
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired")
self.assertCode(resp, 400)
js = parse_resp(resp)
@@ -92,12 +95,50 @@ class BlueprintsTest(TestCase):
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired&example=a")
self.assertCode(resp, 200)
def test_analysis_url(self):
"""
The algorithm can also be specified as part of the URL
"""
self.app.config['TESTING'] = False # Errors are expected in this case
resp = self.client.get("/api/DummyRequired?i=My aloha mohame")
self.assertCode(resp, 400)
js = parse_resp(resp)
logging.debug("Got response: %s", js)
assert isinstance(js, models.Error)
resp = self.client.get("/api/DummyRequired?i=My aloha mohame&example=notvalid")
self.assertCode(resp, 400)
resp = self.client.get("/api/DummyRequired?i=My aloha mohame&example=a")
self.assertCode(resp, 200)
def test_analysis_chain(self):
"""
More than one algorithm can be specified. Plugins will then be chained
"""
resp = self.client.get("/api/Dummy?i=My aloha mohame")
js = parse_resp(resp)
assert len(js['analysis']) == 1
assert js['entries'][0]['nif:isString'] == 'My aloha mohame'[::-1]
resp = self.client.get("/api/Dummy/Dummy?i=My aloha mohame")
# Calling dummy twice, should return the same string
self.assertCode(resp, 200)
js = parse_resp(resp)
assert len(js['analysis']) == 2
assert js['entries'][0]['nif:isString'] == 'My aloha mohame'
resp = self.client.get("/api/Dummy+Dummy?i=My aloha mohame")
# Same with pluses instead of slashes
self.assertCode(resp, 200)
js = parse_resp(resp)
assert len(js['analysis']) == 2
assert js['entries'][0]['nif:isString'] == 'My aloha mohame'
def test_error(self):
"""
The dummy plugin returns an empty response,\
it should contain the context
"""
self.app.debug = False
self.app.config['TESTING'] = False # Errors are expected in this case
resp = self.client.get("/api/?i=My aloha mohame&algo=DOESNOTEXIST")
self.assertCode(resp, 404)
js = parse_resp(resp)
@@ -139,7 +180,7 @@ class BlueprintsTest(TestCase):
js = parse_resp(resp)
logging.debug(js)
assert "@id" in js
assert js["@id"] == "plugins/Dummy_0.1"
assert js["@id"] == "endpoint:plugins/Dummy_0.1"
def test_default(self):
""" Show only one plugin"""
@@ -148,7 +189,7 @@ class BlueprintsTest(TestCase):
js = parse_resp(resp)
logging.debug(js)
assert "@id" in js
assert js["@id"] == "plugins/Dummy_0.1"
assert js["@id"] == "endpoint:plugins/Dummy_0.1"
def test_context(self):
resp = self.client.get("/api/contexts/context.jsonld")
@@ -172,5 +213,6 @@ class BlueprintsTest(TestCase):
assert "help" in js["valid_parameters"]
def test_conversion(self):
self.app.config['TESTING'] = False # Errors are expected in this case
resp = self.client.get("/api/?input=hello&algo=emoRand&emotionModel=DOES NOT EXIST")
self.assertCode(resp, 404)

View File

@@ -14,22 +14,15 @@ class ModelsTest(TestCase):
def test_client(self):
endpoint = 'http://dummy/'
client = Client(endpoint)
with patch_requests(Results()) as (request, response):
with patch_requests('http://dummy/', Results()):
resp = client.analyse('hello')
assert isinstance(resp, Results)
request.assert_called_with(
url=endpoint + '/', method='GET', params={'input': 'hello'})
with patch_requests(Error('Nothing')) as (request, response):
with patch_requests('http://dummy/', Error('Nothing')):
try:
client.analyse(input='hello', algorithm='NONEXISTENT')
raise Exception('Exceptions should be raised. This is not golang')
except Error:
pass
request.assert_called_with(
url=endpoint + '/',
method='GET',
params={'input': 'hello',
'algorithm': 'NONEXISTENT'})
def test_plugins(self):
endpoint = 'http://dummy/'
@@ -37,11 +30,8 @@ class ModelsTest(TestCase):
plugins = Plugins()
p1 = AnalysisPlugin({'name': 'AnalysisP1', 'version': 0, 'description': 'No'})
plugins.plugins = [p1, ]
with patch_requests(plugins) as (request, response):
with patch_requests('http://dummy/plugins', plugins):
response = client.plugins()
assert isinstance(response, dict)
assert len(response) == 1
assert "AnalysisP1" in response
request.assert_called_with(
url=endpoint + '/plugins', method='GET',
params={})

View File

@@ -121,8 +121,8 @@ class ExtensionsTest(TestCase):
# Leaf (defaultdict with __setattr__ and __getattr__.
r1 = analyse(self.senpy, algorithm="Dummy", input="tupni", output="tuptuo")
r2 = analyse(self.senpy, input="tupni", output="tuptuo")
assert r1.analysis[0] == "plugins/Dummy_0.1"
assert r2.analysis[0] == "plugins/Dummy_0.1"
assert r1.analysis[0] == "endpoint:plugins/Dummy_0.1"
assert r2.analysis[0] == "endpoint:plugins/Dummy_0.1"
assert r1.entries[0]['nif:isString'] == 'input'
def test_analyse_empty(self):
@@ -156,8 +156,8 @@ class ExtensionsTest(TestCase):
r2 = analyse(self.senpy,
input="tupni",
output="tuptuo")
assert r1.analysis[0] == "plugins/Dummy_0.1"
assert r2.analysis[0] == "plugins/Dummy_0.1"
assert r1.analysis[0] == "endpoint:plugins/Dummy_0.1"
assert r2.analysis[0] == "endpoint:plugins/Dummy_0.1"
assert r1.entries[0]['nif:isString'] == 'input'
def test_analyse_error(self):

View File

@@ -6,7 +6,7 @@ import pickle
import shutil
import tempfile
from unittest import TestCase, skipIf
from unittest import TestCase
from senpy.models import Results, Entry, EmotionSet, Emotion, Plugins
from senpy import plugins
from senpy.plugins.conversion.emotion.centroids import CentroidConversion
@@ -312,9 +312,7 @@ class PluginsTest(TestCase):
res = c._backwards_conversion(e)
assert res["onyx:hasEmotionCategory"] == "c2"
@skipIf(sys.version_info < (3, 0),
reason="requires Python3")
def test_evaluation(self):
def _test_evaluation(self):
testdata = []
for i in range(50):
testdata.append(["good", 1])
@@ -348,6 +346,14 @@ class PluginsTest(TestCase):
smart_metrics = results[0].metrics[0]
assert abs(smart_metrics['accuracy'] - 1) < 0.01
def test_evaluation(self):
if sys.version_info < (3, 0):
with self.assertRaises(Exception) as context:
self._test_evaluation()
self.assertTrue('GSITK ' in str(context.exception))
else:
self._test_evaluation()
def make_mini_test(fpath):
def mini_test(self):

View File

@@ -5,28 +5,29 @@ import json
from senpy.testing import patch_requests
from senpy.models import Results
ENDPOINT = 'http://example.com'
class TestTest(TestCase):
def test_patch_text(self):
with patch_requests('hello'):
r = requests.get('http://example.com')
with patch_requests(ENDPOINT, 'hello'):
r = requests.get(ENDPOINT)
assert r.text == 'hello'
assert r.content == 'hello'
def test_patch_json(self):
r = Results()
with patch_requests(r):
res = requests.get('http://example.com')
assert res.content == json.dumps(r.jsonld())
with patch_requests(ENDPOINT, r):
res = requests.get(ENDPOINT)
assert res.text == json.dumps(r.jsonld())
js = res.json()
assert js
assert js['@type'] == r['@type']
def test_patch_dict(self):
r = {'nothing': 'new'}
with patch_requests(r):
res = requests.get('http://example.com')
assert res.content == json.dumps(r)
with patch_requests(ENDPOINT, r):
res = requests.get(ENDPOINT)
assert res.text == json.dumps(r)
js = res.json()
assert js
assert js['nothing'] == 'new'