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

Compare commits

..

1 Commits

Author SHA1 Message Date
J. Fernando Sánchez
d7acf3d67a Fix schema issues and parameter validation 2018-05-16 11:15:13 +02:00
42 changed files with 558 additions and 1240 deletions

View File

@@ -18,8 +18,6 @@ before_script:
stage: test stage: test
script: script:
- make -e test-$PYTHON_VERSION - make -e test-$PYTHON_VERSION
except:
- tags # Avoid unnecessary double testing
test-3.5: test-3.5:
<<: *test_definition <<: *test_definition
@@ -31,19 +29,29 @@ test-2.7:
variables: variables:
PYTHON_VERSION: "2.7" PYTHON_VERSION: "2.7"
push: .image: &image_definition
stage: push stage: push
script: script:
- make -e push - make -e push-$PYTHON_VERSION
only: only:
- tags - tags
- triggers - triggers
- fix-makefiles - fix-makefiles
push-3.5:
<<: *image_definition
variables:
PYTHON_VERSION: "3.5"
push-2.7:
<<: *image_definition
variables:
PYTHON_VERSION: "2.7"
push-latest: push-latest:
stage: push <<: *image_definition
script: variables:
- make -e push-latest PYTHON_VERSION: latest
only: only:
- master - master
- triggers - triggers

View File

@@ -1,14 +1,5 @@
ifndef IMAGENAME IMAGENAME?=$(NAME)
ifdef CI_REGISTRY_IMAGE
IMAGENAME=$(CI_REGISTRY_IMAGE)
else
IMAGENAME=$(NAME)
endif
endif
IMAGEWTAG?=$(IMAGENAME):$(VERSION) IMAGEWTAG?=$(IMAGENAME):$(VERSION)
DOCKER_FLAGS?=$(-ti)
DOCKER_CMD?=
docker-login: ## Log in to the registry. It will only be used in the server, or when running a CI task locally (if CI_BUILD_TOKEN is set). docker-login: ## Log in to the registry. It will only be used in the server, or when running a CI task locally (if CI_BUILD_TOKEN is set).
ifeq ($(CI_BUILD_TOKEN),) ifeq ($(CI_BUILD_TOKEN),)
@@ -28,19 +19,6 @@ else
@docker logout @docker logout
endif endif
docker-run: ## Build a generic docker image
docker run $(DOCKER_FLAGS) $(IMAGEWTAG) $(DOCKER_CMD)
docker-build: ## Build a generic docker image
docker build . -t $(IMAGEWTAG)
docker-push: docker-login ## Push a generic docker image
docker push $(IMAGEWTAG)
docker-latest-push: docker-login ## Push the latest image
docker tag $(IMAGEWTAG) $(IMAGENAME)
docker push $(IMAGENAME)
login:: docker-login login:: docker-login
clean:: docker-clean clean:: docker-clean

View File

@@ -14,7 +14,7 @@ push-github: ## Push the code to github. You need to set up GITHUB_DEPLOY_KEY
ifeq ($(GITHUB_DEPLOY_KEY),) ifeq ($(GITHUB_DEPLOY_KEY),)
else else
$(eval KEY_FILE := "$(shell mktemp)") $(eval KEY_FILE := "$(shell mktemp)")
@printf '%b' '$(GITHUB_DEPLOY_KEY)' > $(KEY_FILE) @echo "$(GITHUB_DEPLOY_KEY)" > $(KEY_FILE)
@git remote rm github-deploy || true @git remote rm github-deploy || true
git remote add github-deploy $(GITHUB_REPO) git remote add github-deploy $(GITHUB_REPO)
-@GIT_SSH_COMMAND="ssh -i $(KEY_FILE)" git fetch github-deploy $(CI_COMMIT_REF_NAME) -@GIT_SSH_COMMAND="ssh -i $(KEY_FILE)" git fetch github-deploy $(CI_COMMIT_REF_NAME)
@@ -22,4 +22,7 @@ else
rm $(KEY_FILE) rm $(KEY_FILE)
endif endif
.PHONY:: commit tag git-push git-pull push-github push:: git-push
pull:: git-pull
.PHONY:: commit tag push git-push git-pull push-github

View File

@@ -13,7 +13,7 @@
KUBE_CA_TEMP=false KUBE_CA_TEMP=false
ifndef KUBE_CA_PEM_FILE ifndef KUBE_CA_PEM_FILE
KUBE_CA_PEM_FILE:=$$PWD/.ca.crt KUBE_CA_PEM_FILE:=$$PWD/.ca.crt
CREATED:=$(shell printf '%b\n' '$(KUBE_CA_BUNDLE)' > $(KUBE_CA_PEM_FILE)) CREATED:=$(shell echo -e "$(KUBE_CA_BUNDLE)" > $(KUBE_CA_PEM_FILE))
endif endif
KUBE_TOKEN?="" KUBE_TOKEN?=""
KUBE_NAMESPACE?=$(NAME) KUBE_NAMESPACE?=$(NAME)

View File

@@ -1,15 +1,17 @@
makefiles-remote: makefiles-remote:
git ls-remote --exit-code makefiles 2> /dev/null || git remote add makefiles ssh://git@lab.cluster.gsi.dit.upm.es:2200/docs/templates/makefiles.git @git remote add makefiles ssh://git@lab.cluster.gsi.dit.upm.es:2200/docs/templates/makefiles.git 2>/dev/null || true
makefiles-commit: makefiles-remote makefiles-commit: makefiles-remote
git add -f .makefiles git add -f .makefiles
git commit -em "Updated makefiles from ${NAME}" git commit -em "Updated makefiles from ${NAME}"
makefiles-push: makefiles-push:
git fetch makefiles $(NAME)
git subtree push --prefix=.makefiles/ makefiles $(NAME) git subtree push --prefix=.makefiles/ makefiles $(NAME)
makefiles-pull: makefiles-remote makefiles-pull: makefiles-remote
git subtree pull --prefix=.makefiles/ makefiles master --squash git subtree pull --prefix=.makefiles/ makefiles master --squash
.PHONY:: makefiles-remote makefiles-commit makefiles-push makefiles-pull pull:: makefiles-pull
push:: makefiles-push
.PHONY:: makefiles-remote makefiles-commit makefiles-push makefiles-pull pull push

View File

@@ -26,7 +26,6 @@ Dockerfile-%: Dockerfile.template ## Generate a specific dockerfile (e.g. Docke
quick_build: $(addprefix build-, $(PYMAIN)) quick_build: $(addprefix build-, $(PYMAIN))
build: $(addprefix build-, $(PYVERSIONS)) ## Build all images / python versions build: $(addprefix build-, $(PYVERSIONS)) ## Build all images / python versions
docker tag $(IMAGEWTAG)-python$(PYMAIN) $(IMAGEWTAG)
build-%: version Dockerfile-% ## Build a specific version (e.g. build-2.7) build-%: version Dockerfile-% ## Build a specific version (e.g. build-2.7)
docker build -t '$(IMAGEWTAG)-python$*' -f Dockerfile-$* .; docker build -t '$(IMAGEWTAG)-python$*' -f Dockerfile-$* .;
@@ -76,9 +75,8 @@ pip_upload: pip_test ## Upload package to pip
push-latest: $(addprefix push-latest-,$(PYVERSIONS)) ## Push the "latest" tag to dockerhub push-latest: $(addprefix push-latest-,$(PYVERSIONS)) ## Push the "latest" tag to dockerhub
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGEWTAG)' docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGEWTAG)'
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGENAME):latest' docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGENAME)'
docker push '$(IMAGENAME):latest' docker push '$(IMAGENAME):latest'
docker push '$(IMAGEWTAG)'
push-latest-%: build-% ## Push the latest image for a specific python version push-latest-%: build-% ## Push the latest image for a specific python version
docker tag $(IMAGENAME):$(VERSION)-python$* $(IMAGENAME):python$* docker tag $(IMAGENAME):$(VERSION)-python$* $(IMAGENAME):python$*

View File

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

View File

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

View File

@@ -1,11 +0,0 @@
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

@@ -11,7 +11,7 @@ class Async(AnalysisPlugin):
'''An example of an asynchronous module''' '''An example of an asynchronous module'''
author = '@balkian' author = '@balkian'
version = '0.2' version = '0.2'
sync = False async = True
def _do_async(self, num_processes): def _do_async(self, num_processes):
pool = multiprocessing.Pool(processes=num_processes) pool = multiprocessing.Pool(processes=num_processes)

View File

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

View File

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

View File

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

View File

@@ -3,8 +3,6 @@ from .models import Error, Results, Entry, from_string
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
boolean = [True, False]
API_PARAMS = { API_PARAMS = {
"algorithm": { "algorithm": {
"aliases": ["algorithms", "a", "algo"], "aliases": ["algorithms", "a", "algo"],
@@ -15,14 +13,14 @@ API_PARAMS = {
"expanded-jsonld": { "expanded-jsonld": {
"@id": "expanded-jsonld", "@id": "expanded-jsonld",
"aliases": ["expanded"], "aliases": ["expanded"],
"options": boolean, "options": "boolean",
"required": True, "required": True,
"default": False "default": False
}, },
"with_parameters": { "with_parameters": {
"aliases": ['withparameters', "aliases": ['withparameters',
'with-parameters'], 'with-parameters'],
"options": boolean, "options": "boolean",
"default": False, "default": False,
"required": True "required": True
}, },
@@ -31,14 +29,14 @@ API_PARAMS = {
"aliases": ["o"], "aliases": ["o"],
"default": "json-ld", "default": "json-ld",
"required": True, "required": True,
"options": ["json-ld", "turtle", "ntriples"], "options": ["json-ld", "turtle"],
}, },
"help": { "help": {
"@id": "help", "@id": "help",
"description": "Show additional help to know more about the possible parameters", "description": "Show additional help to know more about the possible parameters",
"aliases": ["h"], "aliases": ["h"],
"required": True, "required": True,
"options": boolean, "options": "boolean",
"default": False "default": False
}, },
"emotionModel": { "emotionModel": {
@@ -85,7 +83,7 @@ WEB_PARAMS = {
"aliases": ["headers"], "aliases": ["headers"],
"required": True, "required": True,
"default": False, "default": False,
"options": boolean "options": "boolean"
}, },
} }
@@ -134,19 +132,10 @@ NIF_PARAMS = {
"aliases": ["u"], "aliases": ["u"],
"required": False, "required": False,
"default": "RFC5147String", "default": "RFC5147String",
"options": ["RFC5147String", ] "options": "RFC5147String"
} }
} }
BUILTIN_PARAMS = {}
for d in [
NIF_PARAMS, CLI_PARAMS, WEB_PARAMS, PLUGINS_PARAMS, EVAL_PARAMS,
API_PARAMS
]:
for k, v in d.items():
BUILTIN_PARAMS[k] = v
def parse_params(indict, *specs): def parse_params(indict, *specs):
if not specs: if not specs:
@@ -170,8 +159,8 @@ def parse_params(indict, *specs):
wrong_params[param] = spec[param] wrong_params[param] = spec[param]
continue continue
if "options" in options: if "options" in options:
if options["options"] == boolean: if options["options"] == "boolean":
outdict[param] = str(outdict[param]).lower() in ['true', '1'] outdict[param] = outdict[param] in [None, True, 'true', '1']
elif outdict[param] not in options["options"]: elif outdict[param] not in options["options"]:
wrong_params[param] = spec[param] wrong_params[param] = spec[param]
if wrong_params: if wrong_params:
@@ -182,24 +171,16 @@ def parse_params(indict, *specs):
parameters=outdict, parameters=outdict,
errors=wrong_params) errors=wrong_params)
raise message raise message
if 'algorithm' in outdict and not isinstance(outdict['algorithm'], tuple): if 'algorithm' in outdict and not isinstance(outdict['algorithm'], list):
outdict['algorithm'] = tuple(outdict['algorithm'].split(',')) outdict['algorithm'] = outdict['algorithm'].split(',')
return outdict return outdict
def parse_extra_params(request, plugins=None): def parse_extra_params(request, plugin=None):
plugins = plugins or []
params = request.parameters.copy() params = request.parameters.copy()
for plugin in plugins: if plugin:
if plugin: extra_params = parse_params(params, plugin.get('extra_params', {}))
extra_params = parse_params(params, plugin.get('extra_params', {})) params.update(extra_params)
for k, v in extra_params.items():
if k not in BUILTIN_PARAMS:
if k in params: # Set by another plugin
del params[k]
else:
params[k] = v
params['{}.{}'.format(plugin.name, k)] = v
return params return params
@@ -209,12 +190,11 @@ def parse_call(params):
params = parse_params(params, NIF_PARAMS) params = parse_params(params, NIF_PARAMS)
if params['informat'] == 'text': if params['informat'] == 'text':
results = Results() results = Results()
entry = Entry(nif__isString=params['input'], id='#') # Use @base entry = Entry(nif__isString=params['input'])
results.entries.append(entry) results.entries.append(entry)
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: # pragma: no cover else: # pragma: no cover
raise NotImplementedError('Informat {} is not implemented'.format( raise NotImplementedError('Informat {} is not implemented'.format(params['informat']))
params['informat']))
results.parameters = params results.parameters = params
return results return results

View File

@@ -18,15 +18,15 @@
Blueprints for Senpy Blueprints for Senpy
""" """
from flask import (Blueprint, request, current_app, render_template, url_for, from flask import (Blueprint, request, current_app, render_template, url_for,
jsonify, redirect) jsonify)
from .models import Error, Response, Help, Plugins, read_schema, dump_schema, Datasets from .models import Error, Response, Help, Plugins, read_schema, dump_schema, Datasets
from . import api from . import api
from .version import __version__ from .version import __version__
from functools import wraps from functools import wraps
import logging import logging
import traceback
import json import json
import base64
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -34,24 +34,6 @@ api_blueprint = Blueprint("api", __name__)
demo_blueprint = Blueprint("demo", __name__, template_folder='templates') demo_blueprint = Blueprint("demo", __name__, template_folder='templates')
ns_blueprint = Blueprint("ns", __name__) 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): def get_params(req):
if req.method == 'POST': if req.method == 'POST':
@@ -63,60 +45,22 @@ def get_params(req):
return indict 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(tuple(request.parameters.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('/') @demo_blueprint.route('/')
def index(): def index():
ev = str(get_params(request).get('evaluation', False)) return render_template("index.html", version=__version__)
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') @api_blueprint.route('/contexts/<entity>.jsonld')
def context(entity="context"): def context(entity="context"):
context = Response._context context = Response._context
context['@vocab'] = url_for('ns.index', _external=True) context['@vocab'] = url_for('ns.index', _external=True)
context['endpoint'] = url_for('api.api_root', _external=True)
return jsonify({"@context": context}) 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 @ns_blueprint.route('/') # noqa: F811
def index(): def index():
context = Response._context.copy() context = Response._context
context['endpoint'] = url_for('api.api_root', _external=True) context['@vocab'] = url_for('.ns', _external=True)
return jsonify({"@context": context}) return jsonify({"@context": context})
@@ -132,7 +76,7 @@ def basic_api(f):
default_params = { default_params = {
'inHeaders': False, 'inHeaders': False,
'expanded-jsonld': False, 'expanded-jsonld': False,
'outformat': None, 'outformat': 'json-ld',
'with_parameters': True, 'with_parameters': True,
} }
@@ -151,55 +95,43 @@ def basic_api(f):
request.parameters = params request.parameters = params
response = f(*args, **kwargs) response = f(*args, **kwargs)
except (Exception) as ex: except (Exception) as ex:
if current_app.debug or current_app.config['TESTING']: if current_app.debug:
raise raise
if not isinstance(ex, Error): if not isinstance(ex, Error):
msg = "{}".format(ex) msg = "{}:\n\t{}".format(ex,
traceback.format_exc())
ex = Error(message=msg, status=500) ex = Error(message=msg, status=500)
logger.exception('Error returning analysis result')
response = ex response = ex
response.parameters = raw_params response.parameters = raw_params
logger.exception(ex) logger.error(ex)
if 'parameters' in response and not params['with_parameters']: if 'parameters' in response and not params['with_parameters']:
del response.parameters del response.parameters
logger.info('Response: {}'.format(response)) 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( return response.flask(
in_headers=params['inHeaders'], in_headers=params['inHeaders'],
headers=headers, headers=headers,
prefix=params.get('prefix', encoded_url()), 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=params['expanded-jsonld']) expanded=params['expanded-jsonld'])
return decorated_function return decorated_function
@api_blueprint.route('/', defaults={'plugin': None}, methods=['POST', 'GET']) @api_blueprint.route('/', methods=['POST', 'GET'])
@api_blueprint.route('/<path:plugin>', methods=['POST', 'GET'])
@basic_api @basic_api
def api_root(plugin): 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(valid_parameters=dic) response = Help(valid_parameters=dic)
return response return response
req = api.parse_call(request.parameters) req = api.parse_call(request.parameters)
if plugin: return current_app.senpy.analyse(req)
plugin = plugin.replace('+', '/')
plugin = plugin.split('/')
req.parameters['algorithm'] = tuple(plugin)
results = current_app.senpy.analyse(req)
results.analysis = set(i.id for i in results.analysis)
return results
@api_blueprint.route('/evaluate/', methods=['POST', 'GET']) @api_blueprint.route('/evaluate/', methods=['POST', 'GET'])
@@ -228,7 +160,7 @@ def plugins():
@api_blueprint.route('/plugins/<plugin>/', methods=['POST', 'GET']) @api_blueprint.route('/plugins/<plugin>/', methods=['POST', 'GET'])
@basic_api @basic_api
def plugin(plugin): def plugin(plugin=None):
sp = current_app.senpy sp = current_app.senpy
return sp.get_plugin(plugin) return sp.get_plugin(plugin)

View File

@@ -13,7 +13,7 @@ class Client(object):
return self.request('/', method=method, input=input, **kwargs) return self.request('/', method=method, input=input, **kwargs)
def evaluate(self, input, method='GET', **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): def plugins(self, *args, **kwargs):
resp = self.request(path='/plugins').plugins resp = self.request(path='/plugins').plugins
@@ -24,12 +24,8 @@ class Client(object):
return {d.name: d for d in resp} return {d.name: d for d in resp}
def request(self, path=None, method='GET', **params): def request(self, path=None, method='GET', **params):
url = '{}{}'.format(self.endpoint.rstrip('/'), path) url = '{}{}'.format(self.endpoint, path)
if method == 'POST': response = requests.request(method=method, url=url, params=params)
response = requests.post(url=url, data=params)
else:
response = requests.request(method=method, url=url, params=params)
try: try:
resp = models.from_dict(response.json()) resp = models.from_dict(response.json())
except Exception as ex: except Exception as ex:

View File

@@ -6,6 +6,7 @@ from future import standard_library
standard_library.install_aliases() standard_library.install_aliases()
from . import plugins, api from . import plugins, api
from .plugins import Plugin, evaluate
from .models import Error, AggregatedEvaluation from .models import Error, AggregatedEvaluation
from .blueprints import api_blueprint, demo_blueprint, ns_blueprint from .blueprints import api_blueprint, demo_blueprint, ns_blueprint
@@ -16,14 +17,19 @@ import copy
import errno import errno
import logging import logging
from . import gsitk_compat
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
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
class Senpy(object): class Senpy(object):
""" Default Senpy extension for Flask """ """ Default Senpy extension for Flask """
def __init__(self, def __init__(self,
app=None, app=None,
plugin_folder=".", plugin_folder=".",
@@ -49,7 +55,7 @@ class Senpy(object):
self.add_folder('plugins', from_root=True) self.add_folder('plugins', from_root=True)
else: else:
# Add only conversion plugins # Add only conversion plugins
self.add_folder(os.path.join('plugins', 'postprocessing'), self.add_folder(os.path.join('plugins', 'conversion'),
from_root=True) from_root=True)
self.app = app self.app = app
if app is not None: if app is not None:
@@ -89,7 +95,7 @@ class Senpy(object):
if plugin in self._plugins: if plugin in self._plugins:
return self._plugins[plugin] return self._plugins[plugin]
results = self.plugins(id='endpoint:plugins/{}'.format(name)) results = self.plugins(id='plugins/{}'.format(name))
if not results: if not results:
return Error(message="Plugin not found", status=404) return Error(message="Plugin not found", status=404)
@@ -114,7 +120,6 @@ class Senpy(object):
raise AttributeError("Not a folder or does not exist: %s", folder) raise AttributeError("Not a folder or does not exist: %s", folder)
def _get_plugins(self, request): def _get_plugins(self, request):
'''Get a list of plugins that should be run for a specific request'''
if not self.analysis_plugins: if not self.analysis_plugins:
raise Error( raise Error(
status=404, status=404,
@@ -132,35 +137,38 @@ class Senpy(object):
plugins = list() plugins = list()
for algo in algos: for algo in algos:
algo = algo.lower() algo = algo.lower()
if algo == 'conversion':
continue # Allow 'conversion' as a virtual plugin, which does nothing
if algo not in self._plugins: if algo not in self._plugins:
msg = ("The algorithm '{}' is not valid\n" msg = ("The algorithm '{}' is not valid\n"
"Valid algorithms: {}").format(algo, "Valid algorithms: {}").format(algo,
self._plugins.keys()) self._plugins.keys())
logger.debug(msg) logger.debug(msg)
raise Error(status=404, message=msg) raise Error(
status=404,
message=msg)
plugins.append(self._plugins[algo]) plugins.append(self._plugins[algo])
return plugins return plugins
def _process(self, req, pending, done=None): def _process_entries(self, entries, req, plugins):
""" """
Recursively process the entries with the first plugin in the list, and pass the results Recursively process the entries with the first plugin in the list, and pass the results
to the rest of the plugins. to the rest of the plugins.
""" """
done = done or [] if not plugins:
if not pending: for i in entries:
return req yield i
return
plugin = pending[0] plugin = plugins[0]
results = plugin.process(req, conversions_applied=done) self._activate(plugin) # Make sure the plugin is activated
if plugin not in results.analysis: specific_params = api.parse_extra_params(req, plugin)
results.analysis.append(plugin) req.analysis.append({'plugin': plugin,
return self._process(results, pending[1:], done) 'parameters': specific_params})
results = plugin.analyse_entries(entries, specific_params)
for i in self._process_entries(results, req, plugins[1:]):
yield i
def install_deps(self): def install_deps(self):
plugins.install_deps(*self.plugins()) for plugin in self.plugins(is_activated=True):
plugins.install_deps(plugin)
def analyse(self, request): def analyse(self, request):
""" """
@@ -169,88 +177,17 @@ class Senpy(object):
by api.parse_call(). by api.parse_call().
""" """
logger.debug("analysing request: {}".format(request)) logger.debug("analysing request: {}".format(request))
entries = request.entries
request.entries = []
plugins = self._get_plugins(request) plugins = self._get_plugins(request)
request.parameters = api.parse_extra_params(request, plugins) results = request
results = self._process(request, plugins) for i in self._process_entries(entries, results, plugins):
logger.debug("Got analysis result: {}".format(results)) results.entries.append(i)
results = self.postprocess(results) self.convert_emotions(results)
logger.debug("Returning post-processed result: {}".format(results)) logger.debug("Returning analysis result: {}".format(results))
results.analysis = [i['plugin'].id for i in results.analysis]
return results return results
def convert_emotions(self, resp):
"""
Conversion of all emotions in a response **in place**.
In addition to converting from one model to another, it has
to include the conversion plugin to the analysis list.
Needless to say, this is far from an elegant solution, but it works.
@todo refactor and clean up
"""
plugins = resp.analysis
params = resp.parameters
toModel = params.get('emotionModel', None)
if not toModel:
return resp
logger.debug('Asked for model: {}'.format(toModel))
output = params.get('conversion', None)
candidates = {}
for plugin in plugins:
try:
fromModel = plugin.get('onyx:usesEmotionModel', None)
candidates[plugin.id] = next(self._conversion_candidates(fromModel, toModel))
logger.debug('Analysis plugin {} uses model: {}'.format(
plugin.id, fromModel))
except StopIteration:
e = Error(('No conversion plugin found for: '
'{} -> {}'.format(fromModel, toModel)),
status=404)
e.original_response = resp
e.parameters = params
raise e
newentries = []
done = []
for i in resp.entries:
if output == "full":
newemotions = copy.deepcopy(i.emotions)
else:
newemotions = []
for j in i.emotions:
plugname = j['prov:wasGeneratedBy']
candidate = candidates[plugname]
done.append({'plugin': candidate, 'parameters': params})
for k in candidate.convert(j, fromModel, toModel, params):
k.prov__wasGeneratedBy = candidate.id
if output == 'nested':
k.prov__wasDerivedFrom = j
newemotions.append(k)
i.emotions = newemotions
newentries.append(i)
resp.entries = newentries
return resp
def _conversion_candidates(self, fromModel, toModel):
candidates = self.plugins(plugin_type=plugins.EmotionConversion)
for candidate in candidates:
for pair in candidate.onyx__doesConversion:
logging.debug(pair)
if candidate.can_convert(fromModel, toModel):
yield candidate
def postprocess(self, response):
'''
Transform the results from the analysis plugins.
It has some pre-defined post-processing like emotion conversion,
and it also allows plugins to auto-select themselves.
'''
response = self.convert_emotions(response)
for plug in self.plugins(plugin_type=plugins.PostProcessing):
if plug.check(response, response.analysis):
response = plug.process(response)
return response
def _get_datasets(self, request): def _get_datasets(self, request):
if not self.datasets: if not self.datasets:
raise Error( raise Error(
@@ -261,19 +198,21 @@ class Senpy(object):
for dataset in datasets_name: for dataset in datasets_name:
if dataset not in self.datasets: if dataset not in self.datasets:
logger.debug(("The dataset '{}' is not valid\n" logger.debug(("The dataset '{}' is not valid\n"
"Valid datasets: {}").format( "Valid datasets: {}").format(dataset,
dataset, self.datasets.keys())) self.datasets.keys()))
raise Error( raise Error(
status=404, status=404,
message="The dataset '{}' is not valid".format(dataset)) message="The dataset '{}' is not valid".format(dataset))
dm = gsitk_compat.DatasetManager() dm = DatasetManager()
datasets = dm.prepare_datasets(datasets_name) datasets = dm.prepare_datasets(datasets_name)
return datasets return datasets
@property @property
def datasets(self): def datasets(self):
if not GSITK_AVAILABLE:
raise Exception('GSITK is not available. Install it to use this function.')
self._dataset_list = {} self._dataset_list = {}
dm = gsitk_compat.DatasetManager() dm = DatasetManager()
for item in dm.get_datasets(): for item in dm.get_datasets():
for key in item: for key in item:
if key in self._dataset_list: if key in self._dataset_list:
@@ -284,23 +223,84 @@ class Senpy(object):
return self._dataset_list return self._dataset_list
def evaluate(self, params): 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)) logger.debug("evaluating request: {}".format(params))
results = AggregatedEvaluation() results = AggregatedEvaluation()
results.parameters = params results.parameters = params
datasets = self._get_datasets(results) datasets = self._get_datasets(results)
plugins = self._get_plugins(results) plugins = self._get_plugins(results)
for eval in plugins.evaluate(plugins, datasets): for eval in evaluate(plugins, datasets):
results.evaluations.append(eval) results.evaluations.append(eval)
if 'with_parameters' not in results.parameters: if 'with_parameters' not in results.parameters:
del results.parameters del results.parameters
logger.debug("Returning evaluation result: {}".format(results)) logger.debug("Returning evaluation result: {}".format(results))
return results return results
def _conversion_candidates(self, fromModel, toModel):
candidates = self.plugins(plugin_type='emotionConversionPlugin')
for candidate in candidates:
for pair in candidate.onyx__doesConversion:
logging.debug(pair)
if pair['onyx:conversionFrom'] == fromModel \
and pair['onyx:conversionTo'] == toModel:
yield candidate
def convert_emotions(self, resp):
"""
Conversion of all emotions in a response **in place**.
In addition to converting from one model to another, it has
to include the conversion plugin to the analysis list.
Needless to say, this is far from an elegant solution, but it works.
@todo refactor and clean up
"""
plugins = [i['plugin'] for i in resp.analysis]
params = resp.parameters
toModel = params.get('emotionModel', None)
if not toModel:
return
logger.debug('Asked for model: {}'.format(toModel))
output = params.get('conversion', None)
candidates = {}
for plugin in plugins:
try:
fromModel = plugin.get('onyx:usesEmotionModel', None)
candidates[plugin.id] = next(self._conversion_candidates(fromModel, toModel))
logger.debug('Analysis plugin {} uses model: {}'.format(plugin.id, fromModel))
except StopIteration:
e = Error(('No conversion plugin found for: '
'{} -> {}'.format(fromModel, toModel)),
status=404)
e.original_response = resp
e.parameters = params
raise e
newentries = []
for i in resp.entries:
if output == "full":
newemotions = copy.deepcopy(i.emotions)
else:
newemotions = []
for j in i.emotions:
plugname = j['prov:wasGeneratedBy']
candidate = candidates[plugname]
resp.analysis.append({'plugin': candidate,
'parameters': params})
for k in candidate.convert(j, fromModel, toModel, params):
k.prov__wasGeneratedBy = candidate.id
if output == 'nested':
k.prov__wasDerivedFrom = j
newemotions.append(k)
i.emotions = newemotions
newentries.append(i)
resp.entries = newentries
@property @property
def default_plugin(self): def default_plugin(self):
if not self._default or not self._default.is_activated: if not self._default or not self._default.is_activated:
candidates = self.plugins( candidates = self.plugins(plugin_type='analysisPlugin',
plugin_type='analysisPlugin', is_activated=True) is_activated=True)
if len(candidates) > 0: if len(candidates) > 0:
self._default = candidates[0] self._default = candidates[0]
else: else:
@@ -310,7 +310,7 @@ class Senpy(object):
@default_plugin.setter @default_plugin.setter
def default_plugin(self, value): def default_plugin(self, value):
if isinstance(value, plugins.Plugin): if isinstance(value, Plugin):
if not value.is_activated: if not value.is_activated:
raise AttributeError('The default plugin has to be activated.') raise AttributeError('The default plugin has to be activated.')
self._default = value self._default = value
@@ -318,15 +318,10 @@ class Senpy(object):
else: else:
self._default = self._plugins[value.lower()] self._default = self._plugins[value.lower()]
def activate_all(self, sync=True, allow_fail=False): def activate_all(self, sync=True):
ps = [] ps = []
for plug in self._plugins.keys(): for plug in self._plugins.keys():
try: ps.append(self.activate_plugin(plug, sync=sync))
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 return ps
def deactivate_all(self, sync=True): def deactivate_all(self, sync=True):
@@ -351,7 +346,6 @@ class Senpy(object):
logger.info(msg) logger.info(msg)
success = True success = True
self._set_active(plugin, success) self._set_active(plugin, success)
return success
def activate_plugin(self, plugin_name, sync=True): def activate_plugin(self, plugin_name, sync=True):
plugin_name = plugin_name.lower() plugin_name = plugin_name.lower()
@@ -362,9 +356,8 @@ class Senpy(object):
logger.info("Activating plugin: {}".format(plugin.name)) logger.info("Activating plugin: {}".format(plugin.name))
if sync or not getattr(plugin, 'async', True) or getattr( if sync or 'async' in plugin and not plugin.async:
plugin, 'sync', False): self._activate(plugin)
return self._activate(plugin)
else: else:
th = Thread(target=partial(self._activate, plugin)) th = Thread(target=partial(self._activate, plugin))
th.start() th.start()
@@ -386,8 +379,7 @@ class Senpy(object):
self._set_active(plugin, False) self._set_active(plugin, False)
if sync or not getattr(plugin, 'async', True) or not getattr( if sync or 'async' in plugin and not plugin.async:
plugin, 'sync', False):
self._deactivate(plugin) self._deactivate(plugin)
else: else:
th = Thread(target=partial(self._deactivate, plugin)) th = Thread(target=partial(self._deactivate, plugin))

View File

@@ -1,31 +0,0 @@
import logging
from pkg_resources import parse_version, get_distribution, DistributionNotFound
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:
gsitk_distro = get_distribution("gsitk")
GSITK_VERSION = parse_version(gsitk_distro.version)
GSITK_AVAILABLE = GSITK_VERSION > parse_version("0.1.9.1") # Earlier versions have a bug
except DistributionNotFound:
GSITK_AVAILABLE = False
GSITK_VERSION = ()
if GSITK_AVAILABLE:
from gsitk.datasets.datasets import DatasetManager
from gsitk.evaluation.evaluation import Evaluation as Eval
from sklearn.pipeline import Pipeline
modules = locals()
else:
logger.warning(IMPORTMSG)
DatasetManager = Eval = Pipeline = raise_exception

View File

@@ -138,7 +138,7 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
@property @property
def id(self): def id(self):
if '@id' not in 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'] return self['@id']
@id.setter @id.setter
@@ -146,7 +146,7 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
self['@id'] = value self['@id'] = value
def flask(self, def flask(self,
in_headers=False, in_headers=True,
headers=None, headers=None,
outformat='json-ld', outformat='json-ld',
**kwargs): **kwargs):
@@ -176,22 +176,20 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
def serialize(self, format='json-ld', with_mime=False, **kwargs): def serialize(self, format='json-ld', with_mime=False, **kwargs):
js = self.jsonld(**kwargs) js = self.jsonld(**kwargs)
content = json.dumps(js, indent=2, sort_keys=True)
if format == 'json-ld': if format == 'json-ld':
content = json.dumps(js, indent=2, sort_keys=True)
mimetype = "application/json" mimetype = "application/json"
elif format in ['turtle', 'ntriples']: elif format in ['turtle', ]:
logger.debug(js) logger.debug(js)
base = kwargs.get('prefix') content = json.dumps(js, indent=2, sort_keys=True)
g = Graph().parse( g = Graph().parse(
data=content, data=content,
format='json-ld', format='json-ld',
base=base, base=kwargs.get('prefix'),
context=[self._context, context=self._context)
{'@base': base}])
logger.debug( logger.debug(
'Parsing with prefix: {}'.format(kwargs.get('prefix'))) 'Parsing with prefix: {}'.format(kwargs.get('prefix')))
content = g.serialize(format=format, content = g.serialize(format='turtle').decode('utf-8')
base=base).decode('utf-8')
mimetype = 'text/{}'.format(format) mimetype = 'text/{}'.format(format)
else: else:
raise Error('Unknown outformat: {}'.format(format)) raise Error('Unknown outformat: {}'.format(format))
@@ -205,23 +203,24 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
context_uri=None, context_uri=None,
prefix=None, prefix=None,
expanded=False): expanded=False):
ser = self.serializable()
result = self.serializable() result = jsonld.compact(
ser,
self._context,
options={
'base': prefix,
'expandContext': self._context,
'senpy': prefix
})
if context_uri:
result['@context'] = context_uri
if expanded: if expanded:
result = jsonld.expand( result = jsonld.expand(
result, options={'base': prefix, result, options={'base': prefix,
'expandContext': self._context})[0] 'expandContext': self._context})
if not with_context: if not with_context:
try: del result['@context']
del result['@context']
except KeyError:
pass
elif context_uri:
result['@context'] = context_uri
else:
result['@context'] = self._context
return result return result
def validate(self, obj=None): def validate(self, obj=None):
@@ -324,10 +323,7 @@ def _add_class_from_schema(*args, **kwargs):
for i in [ for i in [
'aggregatedEvaluation',
'analysis', 'analysis',
'dataset',
'datasets',
'emotion', 'emotion',
'emotionConversion', 'emotionConversion',
'emotionConversionPlugin', 'emotionConversionPlugin',
@@ -335,17 +331,19 @@ for i in [
'emotionModel', 'emotionModel',
'emotionPlugin', 'emotionPlugin',
'emotionSet', 'emotionSet',
'evaluation',
'entity', 'entity',
'help', 'help',
'metric',
'plugin', 'plugin',
'plugins', 'plugins',
'response', 'response',
'results', 'results',
'sentimentPlugin', 'sentimentPlugin',
'suggestion', 'suggestion',
'topic', 'aggregatedEvaluation',
'evaluation',
'metric',
'dataset',
'datasets',
]: ]:
_add_class_from_schema(i) _add_class_from_schema(i)

View File

@@ -1,14 +1,15 @@
from future import standard_library from future import standard_library
standard_library.install_aliases() standard_library.install_aliases()
from future.utils import with_metaclass from future.utils import with_metaclass
from functools import partial
import os.path import os.path
import os import os
import re import re
import pickle import pickle
import logging import logging
import copy
import pprint import pprint
import inspect import inspect
@@ -17,15 +18,21 @@ import subprocess
import importlib import importlib
import yaml import yaml
import threading import threading
import nltk
from .. import models, utils from .. import models, utils
from .. import api from .. import api
from .. import gsitk_compat
from .. import testing
logger = logging.getLogger(__name__) 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): class PluginMeta(models.BaseMeta):
_classes = {} _classes = {}
@@ -43,19 +50,16 @@ class PluginMeta(models.BaseMeta):
if doc: if doc:
attrs['description'] = doc attrs['description'] = doc
else: else:
logger.warning( logger.warn(('Plugin {} does not have a description. '
('Plugin {} does not have a description. ' 'Please, add a short summary to help other developers').format(name))
'Please, add a short summary to help other developers'
).format(name))
cls = super(PluginMeta, mcs).__new__(mcs, name, bases, attrs) cls = super(PluginMeta, mcs).__new__(mcs, name, bases, attrs)
if alias in mcs._classes: if alias in mcs._classes:
if os.environ.get('SENPY_TESTING', ""): if os.environ.get('SENPY_TESTING', ""):
raise Exception( raise Exception(('The type of plugin {} already exists. '
('The type of plugin {} already exists. ' 'Please, choose a different name').format(name))
'Please, choose a different name').format(name))
else: else:
logger.warning('Overloading plugin class: {}'.format(alias)) logger.warn('Overloading plugin class: {}'.format(alias))
mcs._classes[alias] = cls mcs._classes[alias] = cls
return cls return cls
@@ -87,32 +91,10 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
if info: if info:
self.update(info) self.update(info)
self.validate() self.validate()
self.id = 'endpoint:plugins/{}_{}'.format(self['name'], self.id = 'plugins/{}_{}'.format(self['name'], self['version'])
self['version'])
self.is_activated = False self.is_activated = False
self._lock = threading.Lock() self._lock = threading.Lock()
self._directory = os.path.abspath( self.data_folder = data_folder or os.getcwd()
os.path.dirname(inspect.getfile(self.__class__)))
data_folder = data_folder or os.getcwd()
subdir = os.path.join(data_folder, self.name)
self._data_paths = [
data_folder,
subdir,
self._directory,
os.path.join(self._directory, 'data'),
]
if os.path.exists(subdir):
data_folder = subdir
self.data_folder = data_folder
self._log = logging.getLogger('{}.{}'.format(__name__, self.name))
@property
def log(self):
return self._log
def validate(self): def validate(self):
missing = [] missing = []
@@ -120,8 +102,7 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
if x not in self: if x not in self:
missing.append(x) missing.append(x)
if missing: if missing:
raise models.Error( raise models.Error('Missing configuration parameters: {}'.format(missing))
'Missing configuration parameters: {}'.format(missing))
def get_folder(self): def get_folder(self):
return os.path.dirname(inspect.getfile(self.__class__)) return os.path.dirname(inspect.getfile(self.__class__))
@@ -132,108 +113,45 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
def deactivate(self): def deactivate(self):
pass pass
def process(self, request, **kwargs):
"""
An implemented plugin should override this method.
Here, we assume that a process_entries method exists."""
newentries = list(
self.process_entries(request.entries, request.parameters))
request.entries = newentries
return request
def process_entries(self, entries, parameters):
for entry in entries:
self.log.debug('Processing entry with plugin {}: {}'.format(
self, entry))
results = self.process_entry(entry, parameters)
if inspect.isgenerator(results):
for result in results:
yield result
else:
yield results
def process_entry(self, entry, parameters):
"""
This base method is here to adapt plugins which only
implement the *process* function.
Note that this method may yield an annotated entry or a list of
entries (e.g. in a tokenizer)
"""
raise NotImplementedError(
'You need to implement process, process_entries or process_entry in your plugin'
)
def test(self, test_cases=None): def test(self, test_cases=None):
if not test_cases: if not test_cases:
if not hasattr(self, 'test_cases'): if not hasattr(self, 'test_cases'):
raise AttributeError( raise AttributeError(('Plugin {} [{}] does not have any defined '
('Plugin {} [{}] does not have any defined ' 'test cases').format(self.id,
'test cases').format(self.id, inspect.getfile(self.__class__)))
inspect.getfile(self.__class__)))
test_cases = self.test_cases test_cases = self.test_cases
for case in test_cases: for case in test_cases:
try: try:
self.test_case(case) self.test_case(case)
self.log.debug('Test case passed:\n{}'.format( logger.debug('Test case passed:\n{}'.format(pprint.pformat(case)))
pprint.pformat(case)))
except Exception as ex: except Exception as ex:
self.log.warning('Test case failed:\n{}'.format( logger.warn('Test case failed:\n{}'.format(pprint.pformat(case)))
pprint.pformat(case)))
raise raise
def test_case(self, case, mock=testing.MOCK_REQUESTS): def test_case(self, case):
if 'entry' not in case and 'input' in case:
entry = models.Entry(_auto_id=False)
entry.nif__isString = case['input']
case['entry'] = entry
entry = models.Entry(case['entry']) entry = models.Entry(case['entry'])
given_parameters = case.get('params', case.get('parameters', {})) given_parameters = case.get('params', case.get('parameters', {}))
expected = case.get('expected', None) expected = case.get('expected', None)
should_fail = case.get('should_fail', False) should_fail = case.get('should_fail', False)
responses = case.get('responses', [])
try: try:
request = models.Response() params = api.parse_params(given_parameters, self.extra_params)
request.parameters = api.parse_params(given_parameters, res = list(self.analyse_entries([entry, ], params))
self.extra_params)
request.entries = [
entry,
]
method = partial(self.process, request)
if mock:
res = method()
else:
with testing.patch_all_requests(responses):
res = method()
if not isinstance(expected, list): if not isinstance(expected, list):
expected = [expected] expected = [expected]
utils.check_template(res.entries, expected) utils.check_template(res, expected)
res.validate() for r in res:
r.validate()
except models.Error: except models.Error:
if should_fail: if should_fail:
return return
raise raise
assert not should_fail assert not should_fail
def find_file(self, fname): def open(self, fpath, *args, **kwargs):
for p in self._data_paths: if not os.path.isabs(fpath):
alternative = os.path.join(p, fname) fpath = os.path.join(self.data_folder, fpath)
if os.path.exists(alternative): return open(fpath, *args, **kwargs)
return alternative
raise IOError('File does not exist: {}'.format(fname))
def open(self, fpath, mode='r'):
if 'w' in mode:
# When writing, only use absolute paths or data_folder
if not os.path.isabs(fpath):
fpath = os.path.join(self.data_folder, fpath)
else:
fpath = self.find_file(fpath)
return open(fpath, mode=mode)
def serve(self, debug=True, **kwargs): def serve(self, debug=True, **kwargs):
utils.easy(plugin_list=[self, ], plugin_folder=None, debug=debug, **kwargs) utils.easy(plugin_list=[self, ], plugin_folder=None, debug=debug, **kwargs)
@@ -248,26 +166,40 @@ class Analysis(Plugin):
A subclass of Plugin that analyses text and provides an annotation. A subclass of Plugin that analyses text and provides an annotation.
''' '''
def analyse(self, request, parameters): def analyse(self, *args, **kwargs):
return super(Analysis, self).process(request) raise NotImplementedError(
'Your plugin should implement either analyse or analyse_entry')
def analyse_entry(self, entry, parameters):
""" An implemented plugin should override this method.
This base method is here to adapt old style plugins which only
implement the *analyse* function.
Note that this method may yield an annotated entry or a list of
entries (e.g. in a tokenizer)
"""
text = entry['nif:isString']
params = copy.copy(parameters)
params['input'] = text
results = self.analyse(**params)
for i in results.entries:
yield i
def analyse_entries(self, entries, parameters): def analyse_entries(self, entries, parameters):
for i in super(Analysis, self).process_entries(entries, parameters): for entry in entries:
yield i logger.debug('Analysing entry with plugin {}: {}'.format(self, entry))
results = self.analyse_entry(entry, parameters)
if inspect.isgenerator(results):
for result in results:
yield result
else:
yield results
def process(self, request, **kwargs): def test_case(self, case):
return self.analyse(request, request.parameters) if 'entry' not in case and 'input' in case:
entry = models.Entry(_auto_id=False)
def process_entries(self, entries, parameters): entry.nif__isString = case['input']
for i in self.analyse_entries(entries, parameters): case['entry'] = entry
yield i super(Analysis, self).test_case(case)
def process_entry(self, entry, parameters, **kwargs):
if hasattr(self, 'analyse_entry'):
for i in self.analyse_entry(entry, parameters):
yield i
else:
super(Analysis, self).process_entry(entry, parameters, **kwargs)
AnalysisPlugin = Analysis AnalysisPlugin = Analysis
@@ -278,20 +210,7 @@ class Conversion(Plugin):
A subclass of Plugins that convert between different annotation models. A subclass of Plugins that convert between different annotation models.
e.g. a conversion of emotion models, or normalization of sentiment values. e.g. a conversion of emotion models, or normalization of sentiment values.
''' '''
pass
def process(self, response, plugins=None, **kwargs):
plugins = plugins or []
newentries = []
for entry in response.entries:
newentries.append(
self.convert_entry(entry, response.parameters, plugins))
response.entries = newentries
return response
def convert_entry(self, entry, parameters, conversions_applied):
raise NotImplementedError(
'You should implement a way to convert each entry, or a custom process method'
)
ConversionPlugin = Conversion ConversionPlugin = Conversion
@@ -328,28 +247,12 @@ class EmotionConversion(Conversion):
''' '''
A subclass of Conversion that converts emotion annotations using different models A subclass of Conversion that converts emotion annotations using different models
''' '''
pass
def can_convert(self, fromModel, toModel):
'''
Whether this plugin can convert from fromModel to toModel.
If fromModel is None, it is interpreted as "any Model"
'''
for pair in self.onyx__doesConversion:
if (pair['onyx:conversionTo'] == toModel) and \
((fromModel is None) or (pair['onyx:conversionFrom'] == fromModel)):
return True
return False
EmotionConversionPlugin = EmotionConversion EmotionConversionPlugin = EmotionConversion
class PostProcessing(Plugin):
def check(self, request, plugins):
'''Should this plugin be run for this request?'''
return False
class Box(AnalysisPlugin): class Box(AnalysisPlugin):
''' '''
Black box plugins delegate analysis to a function. Black box plugins delegate analysis to a function.
@@ -374,10 +277,9 @@ class Box(AnalysisPlugin):
return output return output
def predict_one(self, input): def predict_one(self, input):
raise NotImplementedError( raise NotImplementedError('You should define the behavior of this plugin')
'You should define the behavior of this plugin')
def process_entries(self, entries, params): def analyse_entries(self, entries, params):
for entry in entries: for entry in entries:
input = self.input(entry=entry, params=params) input = self.input(entry=entry, params=params)
results = self.predict_one(input=input) results = self.predict_one(input=input)
@@ -397,7 +299,7 @@ class Box(AnalysisPlugin):
return self.transform(X) return self.transform(X)
def as_pipe(self): def as_pipe(self):
pipe = gsitk_compat.Pipeline([('plugin', self)]) pipe = Pipeline([('plugin', self)])
pipe.name = self.name pipe.name = self.name
return pipe return pipe
@@ -446,6 +348,7 @@ class EmotionBox(TextBox, EmotionPlugin):
class MappingMixin(object): class MappingMixin(object):
@property @property
def mappings(self): def mappings(self):
return self._mappings return self._mappings
@@ -455,10 +358,11 @@ class MappingMixin(object):
self._mappings = value self._mappings = value
def output(self, output, entry, params): def output(self, output, entry, params):
output = self.mappings.get(output, self.mappings.get( output = self.mappings.get(output,
'default', output)) self.mappings.get('default', output))
return super(MappingMixin, self).output( return super(MappingMixin, self).output(output=output,
output=output, entry=entry, params=params) entry=entry,
params=params)
class ShelfMixin(object): class ShelfMixin(object):
@@ -471,8 +375,7 @@ class ShelfMixin(object):
with self.open(self.shelf_file, 'rb') as p: with self.open(self.shelf_file, 'rb') as p:
self._sh = pickle.load(p) self._sh = pickle.load(p)
except (IndexError, EOFError, pickle.UnpicklingError): except (IndexError, EOFError, pickle.UnpicklingError):
self.log.warning('Corrupted shelf file: {}'.format( logger.warning('{} has a corrupted shelf file!'.format(self.id))
self.shelf_file))
if not self.get('force_shelf', False): if not self.get('force_shelf', False):
raise raise
return self._sh return self._sh
@@ -499,30 +402,32 @@ class ShelfMixin(object):
self._shelf_file = value self._shelf_file = value
def save(self): def save(self):
self.log.debug('Saving pickle') logger.debug('saving pickle')
if hasattr(self, '_sh') and self._sh is not None: if hasattr(self, '_sh') and self._sh is not None:
with self.open(self.shelf_file, 'wb') as f: with self.open(self.shelf_file, 'wb') as f:
pickle.dump(self._sh, f) pickle.dump(self._sh, f)
def pfilter(plugins, plugin_type=Analysis, **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', Plugin)
logger.debug('#' * 100) logger.debug('#' * 100)
logger.debug('plugin_type {}'.format(plugin_type)) logger.debug('ptype {}'.format(ptype))
if plugin_type: if ptype:
if isinstance(plugin_type, PluginMeta): if isinstance(ptype, PluginMeta):
plugin_type = plugin_type.__name__ ptype = ptype.__name__
try: try:
plugin_type = plugin_type[0].upper() + plugin_type[1:] ptype = ptype[0].upper() + ptype[1:]
pclass = globals()[plugin_type] pclass = globals()[ptype]
logger.debug('Class: {}'.format(pclass)) logger.debug('Class: {}'.format(pclass))
candidates = filter(lambda x: isinstance(x, pclass), plugins) candidates = filter(lambda x: isinstance(x, pclass),
plugins)
except KeyError: except KeyError:
raise models.Error('{} is not a valid type'.format(plugin_type)) raise models.Error('{} is not a valid type'.format(ptype))
else: else:
candidates = plugins candidates = plugins
@@ -530,7 +435,8 @@ def pfilter(plugins, plugin_type=Analysis, **kwargs):
def matches(plug): def matches(plug):
res = all(getattr(plug, k, None) == v for (k, v) in kwargs.items()) res = all(getattr(plug, k, None) == v for (k, v) in kwargs.items())
logger.debug("matching {} with {}: {}".format(plug.name, kwargs, res)) logger.debug(
"matching {} with {}: {}".format(plug.name, kwargs, res))
return res return res
if kwargs: if kwargs:
@@ -556,7 +462,6 @@ def _log_subprocess_output(process):
def install_deps(*plugins): def install_deps(*plugins):
installed = False installed = False
nltk_resources = set()
for info in plugins: for info in plugins:
requirements = info.get('requirements', []) requirements = info.get('requirements', [])
if requirements: if requirements:
@@ -564,17 +469,14 @@ def install_deps(*plugins):
for req in requirements: for req in requirements:
pip_args.append(req) pip_args.append(req)
logger.info('Installing requirements: ' + str(requirements)) logger.info('Installing requirements: ' + str(requirements))
process = subprocess.Popen( process = subprocess.Popen(pip_args,
pip_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
_log_subprocess_output(process) _log_subprocess_output(process)
exitcode = process.wait() exitcode = process.wait()
installed = True installed = True
if exitcode != 0: if exitcode != 0:
raise models.Error( raise models.Error("Dependencies not properly installed")
"Dependencies not properly installed: {}".format(pip_args))
nltk_resources |= set(info.get('nltk_resources', []))
installed |= nltk.download(list(nltk_resources))
return installed return installed
@@ -592,7 +494,7 @@ def find_plugins(folders):
yield fpath yield fpath
def from_path(fpath, install_on_fail=False, **kwargs): def from_path(fpath, **kwargs):
logger.debug("Loading plugin from {}".format(fpath)) logger.debug("Loading plugin from {}".format(fpath))
if fpath.endswith('.py'): if fpath.endswith('.py'):
# We asume root is the dir of the file, and module is the name of the file # We asume root is the dir of the file, and module is the name of the file
@@ -602,7 +504,7 @@ def from_path(fpath, install_on_fail=False, **kwargs):
yield instance yield instance
else: else:
info = parse_plugin_info(fpath) info = parse_plugin_info(fpath)
yield from_info(info, install_on_fail=install_on_fail, **kwargs) yield from_info(info, **kwargs)
def from_folder(folders, loader=from_path, **kwargs): def from_folder(folders, loader=from_path, **kwargs):
@@ -613,20 +515,15 @@ def from_folder(folders, loader=from_path, **kwargs):
return plugins return plugins
def from_info(info, root=None, install_on_fail=True, **kwargs): def from_info(info, root=None, **kwargs):
if any(x not in info for x in ('module', )): if any(x not in info for x in ('module',)):
raise ValueError('Plugin info is not valid: {}'.format(info)) raise ValueError('Plugin info is not valid: {}'.format(info))
module = info["module"] module = info["module"]
if not root and '_path' in info: if not root and '_path' in info:
root = os.path.dirname(info['_path']) root = os.path.dirname(info['_path'])
fun = partial(one_from_module, module, root=root, info=info, **kwargs) return 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): def parse_plugin_info(fpath):
@@ -651,8 +548,7 @@ def one_from_module(module, root, info, **kwargs):
if '@type' in info: if '@type' in info:
cls = PluginMeta.from_type(info['@type']) cls = PluginMeta.from_type(info['@type'])
return cls(info=info, **kwargs) return cls(info=info, **kwargs)
instance = next( instance = next(from_module(module=module, root=root, info=info, **kwargs), None)
from_module(module=module, root=root, info=info, **kwargs), None)
if not instance: if not instance:
raise Exception("No valid plugin for: {}".format(module)) raise Exception("No valid plugin for: {}".format(module))
return instance return instance
@@ -674,10 +570,15 @@ def _instances_in_module(module):
yield obj yield obj
def _from_module_name(module, root, info=None, **kwargs): def _from_module_name(module, root, info=None, install=True, **kwargs):
module = load_module(module, root) try:
for plugin in _from_loaded_module( module = load_module(module, root)
module=module, root=root, info=info, **kwargs): except ImportError:
if not install or not info:
raise
install_deps(info)
module = load_module(module, root)
for plugin in _from_loaded_module(module=module, root=root, info=info, **kwargs):
yield plugin yield plugin
@@ -689,10 +590,12 @@ def _from_loaded_module(module, info=None, **kwargs):
def evaluate(plugins, datasets, **kwargs): def evaluate(plugins, datasets, **kwargs):
ev = gsitk_compat.Eval( if not GSITK_AVAILABLE:
tuples=None, raise Exception('GSITK is not available. Install it to use this function.')
datasets=datasets,
pipelines=[plugin.as_pipe() for plugin in plugins]) ev = Eval(tuples=None,
datasets=datasets,
pipelines=[plugin.as_pipe() for plugin in plugins])
ev.evaluate() ev.evaluate()
results = ev.results results = ev.results
evaluations = evaluations_to_JSONLD(results, **kwargs) evaluations = evaluations_to_JSONLD(results, **kwargs)

View File

@@ -1,6 +1,6 @@
--- ---
name: Ekman2FSRE name: Ekman2FSRE
module: senpy.plugins.postprocessing.emotion.centroids module: senpy.plugins.conversion.emotion.centroids
description: Plugin to convert emotion sets from Ekman to VAD description: Plugin to convert emotion sets from Ekman to VAD
version: 0.2 version: 0.2
# No need to specify onyx:doesConversion because centroids.py adds it automatically from centroids_direction # No need to specify onyx:doesConversion because centroids.py adds it automatically from centroids_direction

View File

@@ -1,6 +1,6 @@
--- ---
name: Ekman2PAD name: Ekman2PAD
module: senpy.plugins.postprocessing.emotion.centroids module: senpy.plugins.conversion.emotion.centroids
description: Plugin to convert emotion sets from Ekman to VAD description: Plugin to convert emotion sets from Ekman to VAD
version: 0.2 version: 0.2
# No need to specify onyx:doesConversion because centroids.py adds it automatically from centroids_direction # No need to specify onyx:doesConversion because centroids.py adds it automatically from centroids_direction

View File

@@ -5,27 +5,13 @@ from nltk.tokenize.simple import LineTokenizer
import nltk import nltk
class Split(AnalysisPlugin): class SplitPlugin(AnalysisPlugin):
'''description: A sample plugin that chunks input text''' '''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): def activate(self):
nltk.download('punkt') nltk.download('punkt')
def analyse_entry(self, entry, params): def analyse_entry(self, entry, params):
yield entry
chunker_type = params["delimiter"] chunker_type = params["delimiter"]
original_text = entry['nif:isString'] original_text = entry['nif:isString']
if chunker_type == "sentence": if chunker_type == "sentence":

View File

@@ -0,0 +1,19 @@
---
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

@@ -1,196 +0,0 @@
from senpy import PostProcessing, easy_test
class MaxEmotion(PostProcessing):
'''Plugin to extract the emotion with highest value from an EmotionSet'''
author = '@dsuarezsouto'
version = '0.1'
def process_entry(self, entry, params):
if len(entry.emotions) < 1:
yield entry
return
set_emotions = entry.emotions[0]['onyx:hasEmotion']
# If there is only one emotion, do not modify it
if len(set_emotions) < 2:
yield entry
return
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"
yield entry
def check(self, request, plugins):
return 'maxemotion' in request.parameters and self not in plugins
# Test Cases:
# 1 Normal Situation.
# 2 Case to return a Neutral Emotion.
test_cases = [
{
"name":
"If there are several emotions within an emotion set, reduce it to one.",
"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":
"Test"
},
'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":
"Test"
}
},
{
"name":
"If the maximum emotion has an intensity of 0, return a neutral emotion.",
"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":
"Test"
},
'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":
"Test"
}
}
]
if __name__ == '__main__':
easy_test()

View File

@@ -4,31 +4,12 @@ import json
from senpy.plugins import SentimentPlugin from senpy.plugins import SentimentPlugin
from senpy.models import Sentiment 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''' '''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): def analyse_entry(self, entry, params):
lang = params["language"] lang = params["language"]
res = requests.post(ENDPOINT, res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson",
json.dumps({ json.dumps({
"language": lang, "language": lang,
"data": [{ "data": [{
@@ -50,10 +31,23 @@ class Sentiment140(SentimentPlugin):
marl__hasPolarity=polarity, marl__hasPolarity=polarity,
marl__polarityValue=polarity_value) marl__polarityValue=polarity_value)
sentiment.prov__wasGeneratedBy = self.id sentiment.prov__wasGeneratedBy = self.id
entry.sentiments = []
entry.sentiments.append(sentiment) entry.sentiments.append(sentiment)
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": 4}]}
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': {
@@ -67,9 +61,6 @@ class Sentiment140(SentimentPlugin):
'marl:hasPolarity': 'marl:Positive', 'marl:hasPolarity': 'marl:Positive',
} }
] ]
}, }
'responses': [{'url': ENDPOINT,
'method': 'POST',
'json': {'data': [{'polarity': 4}]}}]
} }
] ]

View File

@@ -0,0 +1,22 @@
---
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

@@ -10,10 +10,8 @@
"wna": "http://www.gsi.dit.upm.es/ontologies/wnaffect/ns#", "wna": "http://www.gsi.dit.upm.es/ontologies/wnaffect/ns#",
"emoml": "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/emotionml/ns#", "emoml": "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/emotionml/ns#",
"xsd": "http://www.w3.org/2001/XMLSchema#", "xsd": "http://www.w3.org/2001/XMLSchema#",
"fam": "http://vocab.fusepool.info/fam#",
"topics": { "topics": {
"@id": "nif:topic", "@id": "dc:subject"
"@container": "@set"
}, },
"entities": { "entities": {
"@id": "me:hasEntities" "@id": "me:hasEntities"

View File

@@ -167,36 +167,3 @@ textarea{
color: inherit; color: inherit;
text-decoration: 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,7 +4,6 @@ var plugins_params = default_params = {};
var plugins = []; var plugins = [];
var defaultPlugin = {}; var defaultPlugin = {};
var gplugins = {}; var gplugins = {};
var pipeline = [];
function replaceURLWithHTMLLinks(text) { function replaceURLWithHTMLLinks(text) {
console.log('Text: ' + text); console.log('Text: ' + text);
@@ -31,10 +30,7 @@ function hashchanged(){
function get_plugins(response){ function get_plugins(response){
for(ix in response.plugins){ plugins = response.plugins;
plug = response.plugins[ix];
plugins[plug.name] = plug;
}
} }
function get_datasets(response){ function get_datasets(response){
@@ -87,32 +83,10 @@ function draw_plugins_selection(){
html += "</optgroup>" html += "</optgroup>"
// Two elements with plugin class // Two elements with plugin class
// One from the evaluate tab and another one from the analyse tab // One from the evaluate tab and another one from the analyse tab
plugin_lists = document.getElementsByClassName('plugin') document.getElementsByClassName('plugin')[0].innerHTML = html;
for (element in plugin_lists){ document.getElementsByClassName('plugin')[1].innerHTML = html;
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(){ function draw_plugins_list(){
var availablePlugins = document.getElementById('availablePlugins'); var availablePlugins = document.getElementById('availablePlugins');
@@ -131,13 +105,6 @@ 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(){ function draw_datasets(){
html = ""; html = "";
repeated_html = "<input class=\"checks-datasets\" type=\"checkbox\" value=\""; repeated_html = "<input class=\"checks-datasets\" type=\"checkbox\" value=\"";
@@ -151,20 +118,16 @@ function draw_datasets(){
$(document).ready(function() { $(document).ready(function() {
var response = JSON.parse($.ajax({type: "GET", url: "/api/plugins/" , async: false}).responseText); 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); 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_plugins(response);
get_default_parameters(); get_default_parameters();
get_datasets(response2);
draw_plugins_list(); draw_plugins_list();
draw_plugins_selection(); draw_plugins_selection();
draw_parameters(); draw_parameters();
draw_plugin_description(); draw_datasets();
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); $(window).on('hashchange', hashchanged);
hashchanged(); hashchanged();
@@ -181,34 +144,17 @@ function get_default_parameters(){
} }
function get_selected_plugin(){
return document.getElementsByClassName('plugin')[0].options[document.getElementsByClassName('plugin')[0].selectedIndex].value;
}
function draw_default_parameters(){ function draw_default_parameters(){
var basic_params = document.getElementById("basic_params"); var basic_params = document.getElementById("basic_params");
basic_params.innerHTML = params_div(default_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(){ function draw_extra_parameters(){
var plugin = get_selected_plugin(); var plugin = document.getElementsByClassName('plugin')[0].options[document.getElementsByClassName('plugin')[0].selectedIndex].value;
get_parameters(); get_parameters();
var extra_params = document.getElementById("extra_params"); var extra_params = document.getElementById("extra_params");
var params = {}; extra_params.innerHTML = params_div(plugins_params[plugin]);
for (sel in pipeline){
update_params(params, pipeline[sel]);
}
update_params(params, plugin);
extra_params.innerHTML = params_div(params);
} }
function draw_parameters(){ function draw_parameters(){
@@ -315,15 +261,6 @@ function add_param(key, value){
return "&"+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(){ function load_JSON(){
url = "/api"; url = "/api";
@@ -332,9 +269,7 @@ function load_JSON(){
rawcontainer.innerHTML = ''; rawcontainer.innerHTML = '';
container.innerHTML = ''; container.innerHTML = '';
var plugin = get_pipeline_arg(); var plugin = document.getElementsByClassName("plugin")[0].options[document.getElementsByClassName("plugin")[0].selectedIndex].value;
$(".loading").addClass("loader");
$("#preview").hide();
var input = encodeURIComponent(document.getElementById("input").value); var input = encodeURIComponent(document.getElementById("input").value);
url += "?algo="+plugin+"&i="+input url += "?algo="+plugin+"&i="+input
@@ -345,27 +280,27 @@ function load_JSON(){
url += add_param(key, params[key]); url += add_param(key, params[key]);
} }
$.ajax({type: "GET", url: url}).always(function(response){ var response = $.ajax({type: "GET", url: url , async: false}).responseText;
document.getElementById("results-div").style.display = 'block'; rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
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();
}
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';
try {
$(".loading").removeClass("loader"); response = JSON.parse(response);
$("#preview").show(); 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';
}
} }
function get_datasets_from_checkbox(){ function get_datasets_from_checkbox(){
@@ -412,53 +347,40 @@ function evaluate_JSON(){
url += "?algo="+plugin+"&dataset="+datasets url += "?algo="+plugin+"&dataset="+datasets
$('#doevaluate').attr("disabled", true); var response = $.ajax({type: "GET", url: url , async: false, dataType: 'json'}).responseText;
$.ajax({type: "GET", url: url, dataType: 'json'}).always(function(resp) { rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
$('#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';
}
})
}
function draw_plugin_description(){ new_tbody = create_body_metrics(response.evaluations)
var plugin = plugins[get_selected_plugin()]; table.replaceChild(new_tbody, table.lastElementChild)
$("#plugdescription").text(plugin.description);
console.log(plugin);
}
function plugin_selected(){ var editor = new JSONEditor(container, options, response);
draw_extra_parameters(); editor.expandAll();
draw_plugin_description(); // $('#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';
}
}

View File

@@ -5,9 +5,6 @@
<title>Playground {{version}}</title> <title>Playground {{version}}</title>
</head> </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="static/js/jquery-2.1.1.min.js" ></script>
<!--<script src="jquery.autosize.min.js"></script>--> <!--<script src="jquery.autosize.min.js"></script>-->
<link rel="stylesheet" href="static/css/bootstrap.min.css"> <link rel="stylesheet" href="static/css/bootstrap.min.css">
@@ -35,9 +32,7 @@
<ul class="nav nav-tabs" role="tablist"> <ul class="nav nav-tabs" role="tablist">
<li role="presentation" ><a class="active" href="#about">About</a></li> <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> <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> <li role="presentation"><a class="active" href="#evaluate">Evaluate Plugins</a></li>
{% endif %}
</ul> </ul>
@@ -66,14 +61,6 @@
</ul> </ul>
</p> </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>
<div class="col-lg-6 "> <div class="col-lg-6 ">
@@ -83,6 +70,8 @@ In Data Science and Advanced Analytics (DSAA),
</div> </div>
<div class="panel-body"><ul id=availablePlugins></ul></div> <div class="panel-body"><ul id=availablePlugins></ul></div>
</div> </div>
</div>
<div class="col-lg-6 ">
<a href="http://senpy.readthedocs.io"> <a href="http://senpy.readthedocs.io">
<div class="panel panel-default"> <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> <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>
@@ -93,6 +82,9 @@ In Data Science and Advanced Analytics (DSAA),
<div class="panel-heading"><i class="fa fa-sign-in"></i> Feel free to follow us on GitHub</div> <div class="panel-heading"><i class="fa fa-sign-in"></i> Feel free to follow us on GitHub</div>
</div> </div>
</a> </a>
<div class="panel panel-default">
<div class="panel-heading"><i class="fa fa-child"></i> Enjoy.</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -104,28 +96,17 @@ In Data Science and Advanced Analytics (DSAA),
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> I cannot believe it!</textarea>
</div> </div>
<div>
<label>Select the plugin:</label>
<select id="plugins" name="plugins" class=plugin onchange="draw_extra_parameters()">
</select>
</div>
<!-- PARAMETERS --> <!-- PARAMETERS -->
<div class="panel-group" id="parameters"> <div class="panel-group" id="parameters">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <a data-toggle="collapse" class="deco-none" href="#basic_params">
<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"> <div class="panel-heading">
<h4 class="panel-title"> <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 Basic API parameters
</h4> </h4>
</div> </div>
@@ -137,8 +118,6 @@ I cannot believe it!</textarea>
<a data-toggle="collapse" class="deco-none" href="#extra_params"> <a data-toggle="collapse" class="deco-none" href="#extra_params">
<div class="panel-heading"> <div class="panel-heading">
<h4 class="panel-title"> <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 Plugin extra parameters
</h4> </h4>
</div> </div>
@@ -150,7 +129,6 @@ I cannot believe it!</textarea>
<!-- END PARAMETERS --> <!-- 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>
<div id="loading-results" class="loading"></div>
<!--<button id="visualise" name="type" type="button">Visualise!</button>--> <!--<button id="visualise" name="type" type="button">Visualise!</button>-->
</form> </form>
</div> </div>
@@ -177,8 +155,6 @@ I cannot believe it!</textarea>
</div> </div>
</div> </div>
{% if evaluation %}
<div class="tab-pane" id="evaluate"> <div class="tab-pane" id="evaluate">
<div class="well"> <div class="well">
<form id="form" class="container" onsubmit="return getPlugins();" accept-charset="utf-8"> <form id="form" class="container" onsubmit="return getPlugins();" accept-charset="utf-8">
@@ -193,7 +169,7 @@ I cannot believe it!</textarea>
</select> </select>
</div> </div>
<a id="doevaluate" class="btn btn-lg btn-primary" onclick="evaluate_JSON()">Evaluate Plugin!</a> <a id="preview" class="btn btn-lg btn-primary" onclick="evaluate_JSON()">Evaluate Plugin!</a>
<!--<button id="visualise" name="type" type="button">Visualise!</button>--> <!--<button id="visualise" name="type" type="button">Visualise!</button>-->
</form> </form>
</div> </div>
@@ -240,7 +216,6 @@ I cannot believe it!</textarea>
</div> </div>
</div> </div>
</div> </div>
{% endif %}
</div> </div>
<a href="http://www.gsi.dit.upm.es" target="_blank"><img class="center-block" src="static/img/gsi.png"/> </a> <a href="http://www.gsi.dit.upm.es" target="_blank"><img class="center-block" src="static/img/gsi.png"/> </a>

36
senpy/test.py Normal file
View File

@@ -0,0 +1,36 @@
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
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)
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

View File

@@ -1,31 +0,0 @@
from past.builtins import basestring
import os
import responses as requestmock
from .models import BaseModel
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:
args['json'] = response
return patch_all_requests([args])

View File

@@ -1,7 +1,6 @@
from . import models, __version__ from . import models, __version__
from collections import MutableMapping from collections import MutableMapping
import pprint import pprint
import pdb
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -33,8 +32,8 @@ def check_template(indict, template):
if indict != template: if indict != template:
raise models.Error(('Differences found.\n' raise models.Error(('Differences found.\n'
'\tExpected: {}\n' '\tExpected: {}\n'
'\tFound: {}').format(pprint.pformat(template), '\tFound: {}').format(pprint.pformat(indict),
pprint.pformat(indict))) pprint.pformat(template)))
def convert_dictionary(original, mappings): def convert_dictionary(original, mappings):
@@ -68,23 +67,18 @@ def easy_load(app=None, plugin_list=None, plugin_folder=None, **kwargs):
return sp, app return sp, app
def easy_test(plugin_list=None, debug=True): def easy_test(plugin_list=None):
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
logging.getLogger().setLevel(logging.INFO) logging.getLogger().setLevel(logging.INFO)
try: if not plugin_list:
if not plugin_list: import __main__
import __main__ logger.info('Loading classes from {}'.format(__main__))
logger.info('Loading classes from {}'.format(__main__)) from . import plugins
from . import plugins plugin_list = plugins.from_module(__main__)
plugin_list = plugins.from_module(__main__) for plug in plugin_list:
for plug in plugin_list: plug.test()
plug.test() logger.info('The tests for {} passed!'.format(plug.name))
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
pdb.post_mortem()
def easy(host='0.0.0.0', port=5000, debug=True, **kwargs): def easy(host='0.0.0.0', port=5000, debug=True, **kwargs):

View File

@@ -21,6 +21,7 @@ class BlueprintsTest(TestCase):
def setUpClass(cls): def setUpClass(cls):
"""Set up only once, and re-use in every individual test""" """Set up only once, and re-use in every individual test"""
cls.app = Flask("test_extensions") cls.app = Flask("test_extensions")
cls.app.debug = False
cls.client = cls.app.test_client() cls.client = cls.app.test_client()
cls.senpy = Senpy(default_plugins=True) cls.senpy = Senpy(default_plugins=True)
cls.senpy.init_app(cls.app) cls.senpy.init_app(cls.app)
@@ -30,21 +31,17 @@ class BlueprintsTest(TestCase):
cls.senpy.activate_plugin("DummyRequired", sync=True) cls.senpy.activate_plugin("DummyRequired", sync=True)
cls.senpy.default_plugin = 'Dummy' cls.senpy.default_plugin = 'Dummy'
def setUp(self):
self.app.config['TESTING'] = True # Tell Flask not to catch Exceptions
def assertCode(self, resp, code): def assertCode(self, resp, code):
self.assertEqual(resp.status_code, code) self.assertEqual(resp.status_code, code)
def test_playground(self): def test_playground(self):
resp = self.client.get("/") resp = self.client.get("/")
assert "main.js" in resp.get_data(as_text=True) assert "main.js" in resp.data.decode()
def test_home(self): def test_home(self):
""" """
Calling with no arguments should ask the user for more arguments 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/") resp = self.client.get("/api/")
self.assertCode(resp, 400) self.assertCode(resp, 400)
js = parse_resp(resp) js = parse_resp(resp)
@@ -67,25 +64,10 @@ class BlueprintsTest(TestCase):
logging.debug("Got response: %s", js) logging.debug("Got response: %s", js)
assert "@context" in js assert "@context" in js
assert "entries" in js assert "entries" in js
assert len(js['analysis']) == 1
def test_analysis_post(self):
"""
The results for a POST request should be the same as for a GET request.
"""
resp = self.client.post("/api/", data={'i': 'My aloha mohame',
'algorithm': 'rand',
'with_parameters': True})
self.assertCode(resp, 200)
js = parse_resp(resp)
logging.debug("Got response: %s", js)
assert "@context" in js
assert "entries" in js
assert len(js['analysis']) == 1
def test_analysis_extra(self): def test_analysis_extra(self):
""" """
Extra params that have a default should use it Extra params that have a default should
""" """
resp = self.client.get("/api/?i=My aloha mohame&algo=Dummy&with_parameters=true") resp = self.client.get("/api/?i=My aloha mohame&algo=Dummy&with_parameters=true")
self.assertCode(resp, 200) self.assertCode(resp, 200)
@@ -99,7 +81,7 @@ class BlueprintsTest(TestCase):
Extra params that have a required argument that does not Extra params that have a required argument that does not
have a default should raise an error. have a default should raise an error.
""" """
self.app.config['TESTING'] = False # Errors are expected in this case self.app.debug = False
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired") resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired")
self.assertCode(resp, 400) self.assertCode(resp, 400)
js = parse_resp(resp) js = parse_resp(resp)
@@ -110,50 +92,12 @@ class BlueprintsTest(TestCase):
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired&example=a") resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired&example=a")
self.assertCode(resp, 200) 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']) == 1
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']) == 1
assert js['entries'][0]['nif:isString'] == 'My aloha mohame'
def test_error(self): def test_error(self):
""" """
The dummy plugin returns an empty response,\ The dummy plugin returns an empty response,\
it should contain the context it should contain the context
""" """
self.app.config['TESTING'] = False # Errors are expected in this case self.app.debug = False
resp = self.client.get("/api/?i=My aloha mohame&algo=DOESNOTEXIST") resp = self.client.get("/api/?i=My aloha mohame&algo=DOESNOTEXIST")
self.assertCode(resp, 404) self.assertCode(resp, 404)
js = parse_resp(resp) js = parse_resp(resp)
@@ -195,7 +139,7 @@ class BlueprintsTest(TestCase):
js = parse_resp(resp) js = parse_resp(resp)
logging.debug(js) logging.debug(js)
assert "@id" in js assert "@id" in js
assert js["@id"] == "endpoint:plugins/Dummy_0.1" assert js["@id"] == "plugins/Dummy_0.1"
def test_default(self): def test_default(self):
""" Show only one plugin""" """ Show only one plugin"""
@@ -204,7 +148,7 @@ class BlueprintsTest(TestCase):
js = parse_resp(resp) js = parse_resp(resp)
logging.debug(js) logging.debug(js)
assert "@id" in js assert "@id" in js
assert js["@id"] == "endpoint:plugins/Dummy_0.1" assert js["@id"] == "plugins/Dummy_0.1"
def test_context(self): def test_context(self):
resp = self.client.get("/api/contexts/context.jsonld") resp = self.client.get("/api/contexts/context.jsonld")
@@ -228,6 +172,5 @@ class BlueprintsTest(TestCase):
assert "help" in js["valid_parameters"] assert "help" in js["valid_parameters"]
def test_conversion(self): 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") resp = self.client.get("/api/?input=hello&algo=emoRand&emotionModel=DOES NOT EXIST")
self.assertCode(resp, 404) self.assertCode(resp, 404)

View File

@@ -1,6 +1,6 @@
from unittest import TestCase from unittest import TestCase
from senpy.testing import patch_requests 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 from senpy.plugins import AnalysisPlugin
@@ -14,28 +14,22 @@ class ModelsTest(TestCase):
def test_client(self): def test_client(self):
endpoint = 'http://dummy/' endpoint = 'http://dummy/'
client = Client(endpoint) client = Client(endpoint)
with patch_requests('http://dummy/', Results()): with patch_requests(Results()) as (request, response):
resp = client.analyse('hello') resp = client.analyse('hello')
assert isinstance(resp, Results) assert isinstance(resp, Results)
with patch_requests('http://dummy/', Error('Nothing')): request.assert_called_with(
url=endpoint + '/', method='GET', params={'input': 'hello'})
with patch_requests(Error('Nothing')) as (request, response):
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
request.assert_called_with(
def test_client_post(self): url=endpoint + '/',
endpoint = 'http://dummy/' method='GET',
client = Client(endpoint) params={'input': 'hello',
with patch_requests('http://dummy/', Results()): 'algorithm': 'NONEXISTENT'})
resp = client.analyse('hello')
assert isinstance(resp, Results)
with patch_requests('http://dummy/', Error('Nothing'), method='POST'):
try:
client.analyse(input='hello', method='POST', algorithm='NONEXISTENT')
raise Exception('Exceptions should be raised. This is not golang')
except Error:
pass
def test_plugins(self): def test_plugins(self):
endpoint = 'http://dummy/' endpoint = 'http://dummy/'
@@ -43,8 +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, ]
with patch_requests('http://dummy/plugins', plugins): with patch_requests(plugins) as (request, response):
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
request.assert_called_with(
url=endpoint + '/plugins', method='GET',
params={})

View File

@@ -47,7 +47,7 @@ class ExtensionsTest(TestCase):
def test_add_delete(self): def test_add_delete(self):
'''Should be able to add and delete new plugins. ''' '''Should be able to add and delete new plugins. '''
new = plugins.Analysis(name='new', description='new', version=0) new = plugins.Plugin(name='new', description='new', version=0)
self.senpy.add_plugin(new) self.senpy.add_plugin(new)
assert new in self.senpy.plugins() assert new in self.senpy.plugins()
self.senpy.delete_plugin(new) self.senpy.delete_plugin(new)
@@ -121,8 +121,8 @@ class ExtensionsTest(TestCase):
# Leaf (defaultdict with __setattr__ and __getattr__. # Leaf (defaultdict with __setattr__ and __getattr__.
r1 = analyse(self.senpy, algorithm="Dummy", input="tupni", output="tuptuo") r1 = analyse(self.senpy, algorithm="Dummy", input="tupni", output="tuptuo")
r2 = analyse(self.senpy, input="tupni", output="tuptuo") r2 = analyse(self.senpy, input="tupni", output="tuptuo")
assert r1.analysis[0].id == "endpoint:plugins/Dummy_0.1" assert r1.analysis[0] == "plugins/Dummy_0.1"
assert r2.analysis[0].id == "endpoint:plugins/Dummy_0.1" assert r2.analysis[0] == "plugins/Dummy_0.1"
assert r1.entries[0]['nif:isString'] == 'input' assert r1.entries[0]['nif:isString'] == 'input'
def test_analyse_empty(self): def test_analyse_empty(self):
@@ -156,8 +156,8 @@ class ExtensionsTest(TestCase):
r2 = analyse(self.senpy, r2 = analyse(self.senpy,
input="tupni", input="tupni",
output="tuptuo") output="tuptuo")
assert r1.analysis[0].id == "endpoint:plugins/Dummy_0.1" assert r1.analysis[0] == "plugins/Dummy_0.1"
assert r2.analysis[0].id == "endpoint:plugins/Dummy_0.1" assert r2.analysis[0] == "plugins/Dummy_0.1"
assert r1.entries[0]['nif:isString'] == 'input' assert r1.entries[0]['nif:isString'] == 'input'
def test_analyse_error(self): def test_analyse_error(self):
@@ -165,7 +165,7 @@ class ExtensionsTest(TestCase):
mm.id = 'magic_mock' mm.id = 'magic_mock'
mm.name = 'mock' mm.name = 'mock'
mm.is_activated = True mm.is_activated = True
mm.process.side_effect = Error('error in analysis', status=500) mm.analyse_entries.side_effect = Error('error in analysis', status=500)
self.senpy.add_plugin(mm) self.senpy.add_plugin(mm)
try: try:
analyse(self.senpy, input='nothing', algorithm='MOCK') analyse(self.senpy, input='nothing', algorithm='MOCK')
@@ -175,7 +175,8 @@ class ExtensionsTest(TestCase):
assert ex['status'] == 500 assert ex['status'] == 500
ex = Exception('generic exception on analysis') ex = Exception('generic exception on analysis')
mm.process.side_effect = ex mm.analyse.side_effect = ex
mm.analyse_entries.side_effect = ex
try: try:
analyse(self.senpy, input='nothing', algorithm='MOCK') analyse(self.senpy, input='nothing', algorithm='MOCK')
@@ -210,28 +211,27 @@ class ExtensionsTest(TestCase):
'emoml:valence': 0 'emoml:valence': 0
})) }))
response = Results({ response = Results({
'analysis': [plugin], 'analysis': [{'plugin': plugin}],
'entries': [Entry({ 'entries': [Entry({
'nif:isString': 'much ado about nothing', 'nif:isString': 'much ado about nothing',
'emotions': [eSet1] 'emotions': [eSet1]
})] })]
}) })
params = {'emotionModel': 'emoml:big6', params = {'emotionModel': 'emoml:big6',
'algorithm': ['conversion'],
'conversion': 'full'} 'conversion': 'full'}
r1 = deepcopy(response) r1 = deepcopy(response)
r1.parameters = params r1.parameters = params
self.senpy.analyse(r1) self.senpy.convert_emotions(r1)
assert len(r1.entries[0].emotions) == 2 assert len(r1.entries[0].emotions) == 2
params['conversion'] = 'nested' params['conversion'] = 'nested'
r2 = deepcopy(response) r2 = deepcopy(response)
r2.parameters = params r2.parameters = params
self.senpy.analyse(r2) self.senpy.convert_emotions(r2)
assert len(r2.entries[0].emotions) == 1 assert len(r2.entries[0].emotions) == 1
assert r2.entries[0].emotions[0]['prov:wasDerivedFrom'] == eSet1 assert r2.entries[0].emotions[0]['prov:wasDerivedFrom'] == eSet1
params['conversion'] = 'filtered' params['conversion'] = 'filtered'
r3 = deepcopy(response) r3 = deepcopy(response)
r3.parameters = params r3.parameters = params
self.senpy.analyse(r3) self.senpy.convert_emotions(r3)
assert len(r3.entries[0].emotions) == 1 assert len(r3.entries[0].emotions) == 1
r3.jsonld() r3.jsonld()

View File

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

View File

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