mirror of
https://github.com/gsi-upm/senpy
synced 2025-10-19 17:58:28 +00:00
Compare commits
46 Commits
pre-1.0
...
mock-reque
Author | SHA1 | Date | |
---|---|---|---|
|
4291c5eabf | ||
|
7c7a815d1a | ||
|
a3eb8f196c | ||
|
00ffbb3804 | ||
|
13cf0c71c5 | ||
|
e5662d482e | ||
|
61181db199 | ||
|
a1663a3f31 | ||
|
83b23dbdf4 | ||
|
4675d9acf1 | ||
|
6832a2816d | ||
|
7a8abf1823 | ||
|
a21ce0d90e | ||
|
a964e586d7 | ||
|
bce42b5bb4 | ||
|
1313853788 | ||
|
697e779767 | ||
|
48f5ffafa1 | ||
|
73f7cbbe8a | ||
|
07a41236f8 | ||
|
55db97cf62 | ||
|
d8dead1908 | ||
|
87dcdb9fbc | ||
|
67ef4b60bd | ||
|
da4b11e5b5 | ||
|
c0aa7ddc3c | ||
|
5e2ada1654 | ||
|
7a188586c5 | ||
|
b768b215c5 | ||
|
d1f1b9a15a | ||
|
52a0f3f4c8 | ||
|
55c32dcd7c | ||
|
0093bc34d5 | ||
|
67bae9a20d | ||
|
551a5cb176 | ||
|
d6f4cc2dd2 | ||
|
4af692091a | ||
|
ec68ff0b90 | ||
|
738da490db | ||
|
d29c42fd2e | ||
|
23c88d0acc | ||
|
dcaaa591b7 | ||
|
15ab5f4c25 | ||
|
92189822d8 | ||
|
fbb418c365 | ||
|
081078ddd6 |
@@ -18,6 +18,8 @@ before_script:
|
||||
stage: test
|
||||
script:
|
||||
- make -e test-$PYTHON_VERSION
|
||||
except:
|
||||
- tags # Avoid unnecessary double testing
|
||||
|
||||
test-3.5:
|
||||
<<: *test_definition
|
||||
|
@@ -1,5 +1,14 @@
|
||||
IMAGENAME?=$(NAME)
|
||||
ifndef IMAGENAME
|
||||
ifdef CI_REGISTRY_IMAGE
|
||||
IMAGENAME=$(CI_REGISTRY_IMAGE)
|
||||
else
|
||||
IMAGENAME=$(NAME)
|
||||
endif
|
||||
endif
|
||||
|
||||
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).
|
||||
ifeq ($(CI_BUILD_TOKEN),)
|
||||
@@ -19,6 +28,19 @@ else
|
||||
@docker logout
|
||||
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
|
||||
|
||||
clean:: docker-clean
|
||||
|
@@ -14,7 +14,7 @@ push-github: ## Push the code to github. You need to set up GITHUB_DEPLOY_KEY
|
||||
ifeq ($(GITHUB_DEPLOY_KEY),)
|
||||
else
|
||||
$(eval KEY_FILE := "$(shell mktemp)")
|
||||
@echo "$(GITHUB_DEPLOY_KEY)" > $(KEY_FILE)
|
||||
@printf '%b' '$(GITHUB_DEPLOY_KEY)' > $(KEY_FILE)
|
||||
@git remote rm github-deploy || true
|
||||
git remote add github-deploy $(GITHUB_REPO)
|
||||
-@GIT_SSH_COMMAND="ssh -i $(KEY_FILE)" git fetch github-deploy $(CI_COMMIT_REF_NAME)
|
||||
|
@@ -13,7 +13,7 @@
|
||||
KUBE_CA_TEMP=false
|
||||
ifndef KUBE_CA_PEM_FILE
|
||||
KUBE_CA_PEM_FILE:=$$PWD/.ca.crt
|
||||
CREATED:=$(shell echo -e "$(KUBE_CA_BUNDLE)" > $(KUBE_CA_PEM_FILE))
|
||||
CREATED:=$(shell printf '%b\n' '$(KUBE_CA_BUNDLE)' > $(KUBE_CA_PEM_FILE))
|
||||
endif
|
||||
KUBE_TOKEN?=""
|
||||
KUBE_NAMESPACE?=$(NAME)
|
||||
|
@@ -26,6 +26,7 @@ Dockerfile-%: Dockerfile.template ## Generate a specific dockerfile (e.g. Docke
|
||||
quick_build: $(addprefix build-, $(PYMAIN))
|
||||
|
||||
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)
|
||||
docker build -t '$(IMAGEWTAG)-python$*' -f Dockerfile-$* .;
|
||||
@@ -75,7 +76,7 @@ pip_upload: pip_test ## Upload package to pip
|
||||
|
||||
push-latest: $(addprefix push-latest-,$(PYVERSIONS)) ## Push the "latest" tag to dockerhub
|
||||
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGEWTAG)'
|
||||
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGENAME)'
|
||||
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGENAME):latest'
|
||||
docker push '$(IMAGENAME):latest'
|
||||
docker push '$(IMAGEWTAG)'
|
||||
|
||||
|
@@ -6,8 +6,6 @@ RUN apt-get update && apt-get install -y \
|
||||
libblas-dev liblapack-dev liblapacke-dev gfortran \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip install --no-cache-dir --upgrade numpy scipy scikit-learn
|
||||
|
||||
RUN mkdir /cache/ /senpy-plugins /data/
|
||||
|
||||
VOLUME /data/
|
||||
@@ -20,8 +18,8 @@ ONBUILD WORKDIR /senpy-plugins/
|
||||
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY test-requirements.txt requirements.txt /usr/src/app/
|
||||
RUN pip install --no-cache-dir --use-wheel -r test-requirements.txt -r requirements.txt
|
||||
COPY test-requirements.txt requirements.txt extra-requirements.txt /usr/src/app/
|
||||
RUN pip install --no-cache-dir -r test-requirements.txt -r requirements.txt -r extra-requirements.txt
|
||||
COPY . /usr/src/app/
|
||||
RUN pip install --no-cache-dir --no-index --no-deps --editable .
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
include requirements.txt
|
||||
include test-requirements.txt
|
||||
include extra-requirements.txt
|
||||
include README.rst
|
||||
include senpy/VERSION
|
||||
graft senpy/plugins
|
||||
|
10
docker-compose.dev.yml
Normal file
10
docker-compose.dev.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
version: '3'
|
||||
services:
|
||||
senpy:
|
||||
image: "${IMAGENAME-gsiupm/senpy}:${VERSION-latest}"
|
||||
entrypoint: ["/bin/bash"]
|
||||
working_dir: "/senpy-plugins"
|
||||
ports:
|
||||
- 5000:5000
|
||||
volumes:
|
||||
- ".:/usr/src/app/"
|
9
docker-compose.test.yml
Normal file
9
docker-compose.test.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
version: '3'
|
||||
services:
|
||||
test:
|
||||
image: "${IMAGENAME-gsiupm/senpy}:${VERSION-dev}"
|
||||
entrypoint: ["py.test"]
|
||||
volumes:
|
||||
- ".:/usr/src/app/"
|
||||
command:
|
||||
[]
|
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
version: '3'
|
||||
services:
|
||||
senpy:
|
||||
image: "${IMAGENAME-gsiupm/senpy}:${VERSION-dev}"
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile${PYVERSION--2.7}
|
||||
ports:
|
||||
- 5001:5000
|
||||
volumes:
|
||||
- "./data:/data"
|
@@ -1,8 +1,11 @@
|
||||
What is Senpy?
|
||||
--------------
|
||||
|
||||
Web services can get really complex: data validation, user interaction, formatting, logging., etc.
|
||||
The figure below summarizes the typical features in an analysis service.
|
||||
Senpy is a framework for text analysis using Linked Data. There are three main applications of Senpy so far: sentiment and emotion analysis, user profiling and entity recoginition. Annotations and Services are compliant with NIF (NLP Interchange Format).
|
||||
|
||||
Senpy aims at providing a framework where analysis modules can be integrated easily as plugins, and providing a core functionality for managing tasks such as data validation, user interaction, formatting, logging, translation to linked data, etc.
|
||||
|
||||
The figure below summarizes the typical features in a text analysis service.
|
||||
Senpy implements all the common blocks, so developers can focus on what really matters: great analysis algorithms that solve real problems.
|
||||
|
||||
.. image:: senpy-framework.png
|
||||
|
@@ -1,8 +1,24 @@
|
||||
Vocabularies and model
|
||||
======================
|
||||
|
||||
The model used in Senpy is based on the following vocabularies:
|
||||
The model used in Senpy is based on NIF 2.0 [1], which defines a semantic format and API for improving interoperability among natural language processing services.
|
||||
|
||||
* Marl, a vocabulary designed to annotate and describe subjetive opinions expressed on the web or in information systems.
|
||||
* Onyx, which is built one the same principles as Marl to annotate and describe emotions, and provides interoperability with Emotion Markup Language.
|
||||
* NIF 2.0, which defines a semantic format and APO for improving interoperability among natural language processing services
|
||||
Senpy has been applied to sentiment and emotion analysis services using the following vocabularies:
|
||||
|
||||
* Marl [2,6], a vocabulary designed to annotate and describe subjetive opinions expressed on the web or in information systems.
|
||||
* Onyx [3,5], which is built one the same principles as Marl to annotate and describe emotions, and provides interoperability with Emotion Markup Language.
|
||||
|
||||
An overview of the vocabularies and their use can be found in [4].
|
||||
|
||||
|
||||
[1] Guidelines for developing NIF-based NLP services, Final Community Group Report 22 December 2015 Available at: https://www.w3.org/2015/09/bpmlod-reports/nif-based-nlp-webservices/
|
||||
|
||||
[2] Marl Ontology Specification, available at http://www.gsi.dit.upm.es/ontologies/marl/
|
||||
|
||||
[3] Onyx Ontology Specification, available at http://www.gsi.dit.upm.es/ontologies/onyx/
|
||||
|
||||
[4] Iglesias, C. A., Sánchez-Rada, J. F., Vulcu, G., & Buitelaar, P. (2017). Linked Data Models for Sentiment and Emotion Analysis in Social Networks. In Sentiment Analysis in Social Networks (pp. 49-69).
|
||||
|
||||
[5] Sánchez-Rada, J. F., & Iglesias, C. A. (2016). Onyx: A linked data approach to emotion representation. Information Processing & Management, 52(1), 99-114.
|
||||
|
||||
[6] Westerski, A., Iglesias Fernandez, C. A., & Tapia Rico, F. (2011). Linked opinions: Describing sentiments on the structured web of data.
|
||||
|
@@ -18,7 +18,7 @@ class BasicBox(SentimentBox):
|
||||
'default': 'marl:Neutral'
|
||||
}
|
||||
|
||||
def box(self, input, **kwargs):
|
||||
def predict_one(self, input):
|
||||
output = basic.get_polarity(input)
|
||||
return self.mappings.get(output, self.mappings['default'])
|
||||
|
||||
|
@@ -18,7 +18,7 @@ class Basic(MappingMixin, SentimentBox):
|
||||
'default': 'marl:Neutral'
|
||||
}
|
||||
|
||||
def box(self, input, **kwargs):
|
||||
def predict_one(self, input):
|
||||
return basic.get_polarity(input)
|
||||
|
||||
test_cases = [{
|
||||
|
@@ -43,7 +43,6 @@ class Dictionary(plugins.SentimentPlugin):
|
||||
|
||||
class EmojiOnly(Dictionary):
|
||||
'''Sentiment annotation with a basic lexicon of emojis'''
|
||||
description = 'A plugin'
|
||||
dictionaries = [basic.emojis]
|
||||
|
||||
test_cases = [{
|
||||
|
@@ -18,7 +18,7 @@ class PipelineSentiment(MappingMixin, SentimentBox):
|
||||
-1: 'marl:Negative'
|
||||
}
|
||||
|
||||
def box(self, input, *args, **kwargs):
|
||||
def predict_one(self, input):
|
||||
return pipeline.predict([input, ])[0]
|
||||
|
||||
test_cases = [
|
||||
|
1
extra-requirements.txt
Normal file
1
extra-requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
gsitk
|
@@ -9,3 +9,7 @@ jsonref
|
||||
PyYAML
|
||||
rdflib
|
||||
rdflib-jsonld
|
||||
numpy
|
||||
scipy
|
||||
scikit-learn
|
||||
responses
|
||||
|
@@ -19,9 +19,6 @@ Sentiment analysis server in Python
|
||||
"""
|
||||
from .version import __version__
|
||||
|
||||
from future.standard_library import install_aliases
|
||||
install_aliases()
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@@ -22,6 +22,7 @@ the server.
|
||||
|
||||
from flask import Flask
|
||||
from senpy.extensions import Senpy
|
||||
from senpy.utils import easy_test
|
||||
|
||||
import logging
|
||||
import os
|
||||
@@ -39,7 +40,7 @@ def main():
|
||||
'-l',
|
||||
metavar='logging_level',
|
||||
type=str,
|
||||
default="ERROR",
|
||||
default="WARN",
|
||||
help='Logging level')
|
||||
parser.add_argument(
|
||||
'--debug',
|
||||
@@ -75,6 +76,17 @@ def main():
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Do not run a server, only install plugin dependencies')
|
||||
parser.add_argument(
|
||||
'--only-test',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Do not run a server, just test all plugins')
|
||||
parser.add_argument(
|
||||
'--test',
|
||||
'-t',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Test all plugins before launching the server')
|
||||
parser.add_argument(
|
||||
'--only-list',
|
||||
'--list',
|
||||
@@ -92,12 +104,24 @@ def main():
|
||||
action='store_false',
|
||||
default=True,
|
||||
help='Run a threaded server')
|
||||
parser.add_argument(
|
||||
'--no-deps',
|
||||
'-n',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Skip installing dependencies')
|
||||
parser.add_argument(
|
||||
'--version',
|
||||
'-v',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Output the senpy version and exit')
|
||||
parser.add_argument(
|
||||
'--allow-fail',
|
||||
'--fail',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Do not exit if some plugins fail to activate')
|
||||
args = parser.parse_args()
|
||||
if args.version:
|
||||
print('Senpy version {}'.format(senpy.__version__))
|
||||
@@ -112,16 +136,27 @@ def main():
|
||||
data_folder=args.data_folder)
|
||||
if args.only_list:
|
||||
plugins = sp.plugins()
|
||||
maxwidth = max(len(x.id) for x in plugins)
|
||||
maxname = max(len(x.name) for x in plugins)
|
||||
maxversion = max(len(x.version) for x in plugins)
|
||||
print('Found {} plugins:'.format(len(plugins)))
|
||||
for plugin in plugins:
|
||||
import inspect
|
||||
fpath = inspect.getfile(plugin.__class__)
|
||||
print('{: <{width}} @ {}'.format(plugin.id, fpath, width=maxwidth))
|
||||
print('\t{: <{maxname}} @ {: <{maxversion}} -> {}'.format(plugin.name,
|
||||
plugin.version,
|
||||
fpath,
|
||||
maxname=maxname,
|
||||
maxversion=maxversion))
|
||||
return
|
||||
if not args.no_deps:
|
||||
sp.install_deps()
|
||||
if args.only_install:
|
||||
return
|
||||
sp.activate_all()
|
||||
sp.activate_all(allow_fail=args.allow_fail)
|
||||
if args.test or args.only_test:
|
||||
easy_test(sp.plugins(), debug=args.debug)
|
||||
if args.only_test:
|
||||
return
|
||||
print('Senpy version {}'.format(senpy.__version__))
|
||||
print('Server running on port %s:%d. Ctrl+C to quit' % (args.host,
|
||||
args.port))
|
||||
|
40
senpy/api.py
40
senpy/api.py
@@ -3,6 +3,10 @@ from .models import Error, Results, Entry, from_string
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
boolean = [True, False]
|
||||
|
||||
|
||||
API_PARAMS = {
|
||||
"algorithm": {
|
||||
"aliases": ["algorithms", "a", "algo"],
|
||||
@@ -13,14 +17,14 @@ API_PARAMS = {
|
||||
"expanded-jsonld": {
|
||||
"@id": "expanded-jsonld",
|
||||
"aliases": ["expanded"],
|
||||
"options": "boolean",
|
||||
"options": boolean,
|
||||
"required": True,
|
||||
"default": False
|
||||
},
|
||||
"with_parameters": {
|
||||
"aliases": ['withparameters',
|
||||
'with-parameters'],
|
||||
"options": "boolean",
|
||||
"options": boolean,
|
||||
"default": False,
|
||||
"required": True
|
||||
},
|
||||
@@ -29,14 +33,14 @@ API_PARAMS = {
|
||||
"aliases": ["o"],
|
||||
"default": "json-ld",
|
||||
"required": True,
|
||||
"options": ["json-ld", "turtle"],
|
||||
"options": ["json-ld", "turtle", "ntriples"],
|
||||
},
|
||||
"help": {
|
||||
"@id": "help",
|
||||
"description": "Show additional help to know more about the possible parameters",
|
||||
"aliases": ["h"],
|
||||
"required": True,
|
||||
"options": "boolean",
|
||||
"options": boolean,
|
||||
"default": False
|
||||
},
|
||||
"emotionModel": {
|
||||
@@ -53,6 +57,21 @@ API_PARAMS = {
|
||||
}
|
||||
}
|
||||
|
||||
EVAL_PARAMS = {
|
||||
"algorithm": {
|
||||
"aliases": ["plug", "p", "plugins", "algorithms", 'algo', 'a', 'plugin'],
|
||||
"description": "Plugins to be evaluated",
|
||||
"required": True,
|
||||
"help": "See activated plugins in /plugins"
|
||||
},
|
||||
"dataset": {
|
||||
"aliases": ["datasets", "data", "d"],
|
||||
"description": "Datasets to be evaluated",
|
||||
"required": True,
|
||||
"help": "See avalaible datasets in /datasets"
|
||||
}
|
||||
}
|
||||
|
||||
PLUGINS_PARAMS = {
|
||||
"plugin_type": {
|
||||
"@id": "pluginType",
|
||||
@@ -68,7 +87,7 @@ WEB_PARAMS = {
|
||||
"aliases": ["headers"],
|
||||
"required": True,
|
||||
"default": False,
|
||||
"options": "boolean"
|
||||
"options": boolean
|
||||
},
|
||||
}
|
||||
|
||||
@@ -117,7 +136,7 @@ NIF_PARAMS = {
|
||||
"aliases": ["u"],
|
||||
"required": False,
|
||||
"default": "RFC5147String",
|
||||
"options": "RFC5147String"
|
||||
"options": ["RFC5147String", ]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +151,7 @@ def parse_params(indict, *specs):
|
||||
for param, options in iteritems(spec):
|
||||
for alias in options.get("aliases", []):
|
||||
# Replace each alias with the correct name of the parameter
|
||||
if alias in indict and alias is not param:
|
||||
if alias in indict and alias != param:
|
||||
outdict[param] = indict[alias]
|
||||
del outdict[alias]
|
||||
continue
|
||||
@@ -144,7 +163,7 @@ def parse_params(indict, *specs):
|
||||
wrong_params[param] = spec[param]
|
||||
continue
|
||||
if "options" in options:
|
||||
if options["options"] == "boolean":
|
||||
if options["options"] == boolean:
|
||||
outdict[param] = outdict[param] in [None, True, 'true', '1']
|
||||
elif outdict[param] not in options["options"]:
|
||||
wrong_params[param] = spec[param]
|
||||
@@ -157,7 +176,7 @@ def parse_params(indict, *specs):
|
||||
errors=wrong_params)
|
||||
raise message
|
||||
if 'algorithm' in outdict and not isinstance(outdict['algorithm'], list):
|
||||
outdict['algorithm'] = outdict['algorithm'].split(',')
|
||||
outdict['algorithm'] = list(outdict['algorithm'].split(','))
|
||||
return outdict
|
||||
|
||||
|
||||
@@ -175,7 +194,8 @@ def parse_call(params):
|
||||
params = parse_params(params, NIF_PARAMS)
|
||||
if params['informat'] == 'text':
|
||||
results = Results()
|
||||
entry = Entry(nif__isString=params['input'])
|
||||
entry = Entry(nif__isString=params['input'],
|
||||
id='#') # Use @base
|
||||
results.entries.append(entry)
|
||||
elif params['informat'] == 'json-ld':
|
||||
results = from_string(params['input'], cls=Results)
|
||||
|
@@ -18,15 +18,15 @@
|
||||
Blueprints for Senpy
|
||||
"""
|
||||
from flask import (Blueprint, request, current_app, render_template, url_for,
|
||||
jsonify)
|
||||
from .models import Error, Response, Help, Plugins, read_schema
|
||||
jsonify, redirect)
|
||||
from .models import Error, Response, Help, Plugins, read_schema, dump_schema, Datasets
|
||||
from . import api
|
||||
from .version import __version__
|
||||
from functools import wraps
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
import json
|
||||
import base64
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -34,6 +34,24 @@ api_blueprint = Blueprint("api", __name__)
|
||||
demo_blueprint = Blueprint("demo", __name__, template_folder='templates')
|
||||
ns_blueprint = Blueprint("ns", __name__)
|
||||
|
||||
_mimetypes_r = {'json-ld': ['application/ld+json'],
|
||||
'turtle': ['text/turtle'],
|
||||
'ntriples': ['application/n-triples'],
|
||||
'text': ['text/plain']}
|
||||
|
||||
MIMETYPES = {}
|
||||
|
||||
for k, vs in _mimetypes_r.items():
|
||||
for v in vs:
|
||||
if v in MIMETYPES:
|
||||
raise Exception('MIMETYPE {} specified for two formats: {} and {}'.format(v,
|
||||
v,
|
||||
MIMETYPES[v]))
|
||||
MIMETYPES[v] = k
|
||||
|
||||
DEFAULT_MIMETYPE = 'application/ld+json'
|
||||
DEFAULT_FORMAT = 'json-ld'
|
||||
|
||||
|
||||
def get_params(req):
|
||||
if req.method == 'POST':
|
||||
@@ -45,38 +63,76 @@ def get_params(req):
|
||||
return indict
|
||||
|
||||
|
||||
def encoded_url(url=None, base=None):
|
||||
code = ''
|
||||
if not url:
|
||||
if request.method == 'GET':
|
||||
url = request.full_path[1:] # Remove the first slash
|
||||
else:
|
||||
hash(frozenset(request.form.params().items()))
|
||||
code = 'hash:{}'.format(hash)
|
||||
|
||||
code = code or base64.urlsafe_b64encode(url.encode()).decode()
|
||||
|
||||
if base:
|
||||
return base + code
|
||||
return url_for('api.decode', code=code, _external=True)
|
||||
|
||||
|
||||
def decoded_url(code, base=None):
|
||||
if code.startswith('hash:'):
|
||||
raise Exception('Can not decode a URL for a POST request')
|
||||
base = base or request.url_root
|
||||
path = base64.urlsafe_b64decode(code.encode()).decode()
|
||||
return base + path
|
||||
|
||||
|
||||
@demo_blueprint.route('/')
|
||||
def index():
|
||||
return render_template("index.html", version=__version__)
|
||||
ev = str(get_params(request).get('evaluation', False))
|
||||
evaluation_enabled = ev.lower() not in ['false', 'no', 'none']
|
||||
|
||||
return render_template("index.html",
|
||||
evaluation=evaluation_enabled,
|
||||
version=__version__)
|
||||
|
||||
|
||||
@api_blueprint.route('/contexts/<entity>.jsonld')
|
||||
def context(entity="context"):
|
||||
context = Response._context
|
||||
context['@vocab'] = url_for('ns.index', _external=True)
|
||||
context['endpoint'] = url_for('api.api_root', _external=True)
|
||||
return jsonify({"@context": context})
|
||||
|
||||
|
||||
@api_blueprint.route('/d/<code>')
|
||||
def decode(code):
|
||||
try:
|
||||
return redirect(decoded_url(code))
|
||||
except Exception:
|
||||
return Error('invalid URL').flask()
|
||||
|
||||
|
||||
@ns_blueprint.route('/') # noqa: F811
|
||||
def index():
|
||||
context = Response._context
|
||||
context['@vocab'] = url_for('.ns', _external=True)
|
||||
context = Response._context.copy()
|
||||
context['endpoint'] = url_for('api.api_root', _external=True)
|
||||
return jsonify({"@context": context})
|
||||
|
||||
|
||||
@api_blueprint.route('/schemas/<schema>')
|
||||
def schema(schema="definitions"):
|
||||
try:
|
||||
return jsonify(read_schema(schema))
|
||||
except Exception: # Should be FileNotFoundError, but it's missing from py2
|
||||
return Error(message="Schema not found", status=404).flask()
|
||||
return dump_schema(read_schema(schema))
|
||||
except Exception as ex: # Should be FileNotFoundError, but it's missing from py2
|
||||
return Error(message="Schema not found: {}".format(ex), status=404).flask()
|
||||
|
||||
|
||||
def basic_api(f):
|
||||
default_params = {
|
||||
'inHeaders': False,
|
||||
'expanded-jsonld': False,
|
||||
'outformat': 'json-ld',
|
||||
'outformat': None,
|
||||
'with_parameters': True,
|
||||
}
|
||||
|
||||
@@ -95,29 +151,34 @@ def basic_api(f):
|
||||
request.parameters = params
|
||||
response = f(*args, **kwargs)
|
||||
except (Exception) as ex:
|
||||
if current_app.debug:
|
||||
if current_app.debug or current_app.config['TESTING']:
|
||||
raise
|
||||
if not isinstance(ex, Error):
|
||||
msg = "{}:\n\t{}".format(ex,
|
||||
traceback.format_exc())
|
||||
msg = "{}".format(ex)
|
||||
ex = Error(message=msg, status=500)
|
||||
logger.exception('Error returning analysis result')
|
||||
response = ex
|
||||
response.parameters = raw_params
|
||||
logger.error(ex)
|
||||
logger.exception(ex)
|
||||
|
||||
if 'parameters' in response and not params['with_parameters']:
|
||||
del response.parameters
|
||||
|
||||
logger.info('Response: {}'.format(response))
|
||||
mime = request.accept_mimetypes\
|
||||
.best_match(MIMETYPES.keys(),
|
||||
DEFAULT_MIMETYPE)
|
||||
|
||||
mimeformat = MIMETYPES.get(mime, DEFAULT_FORMAT)
|
||||
outformat = params['outformat'] or mimeformat
|
||||
|
||||
return response.flask(
|
||||
in_headers=params['inHeaders'],
|
||||
headers=headers,
|
||||
prefix=url_for('.api_root', _external=True),
|
||||
prefix=params.get('prefix', encoded_url()),
|
||||
context_uri=url_for('api.context',
|
||||
entity=type(response).__name__,
|
||||
_external=True),
|
||||
outformat=params['outformat'],
|
||||
outformat=outformat,
|
||||
expanded=params['expanded-jsonld'])
|
||||
|
||||
return decorated_function
|
||||
@@ -134,6 +195,19 @@ def api_root():
|
||||
return current_app.senpy.analyse(req)
|
||||
|
||||
|
||||
@api_blueprint.route('/evaluate/', methods=['POST', 'GET'])
|
||||
@basic_api
|
||||
def evaluate():
|
||||
if request.parameters['help']:
|
||||
dic = dict(api.EVAL_PARAMS)
|
||||
response = Help(parameters=dic)
|
||||
return response
|
||||
else:
|
||||
params = api.parse_params(request.parameters, api.EVAL_PARAMS)
|
||||
response = current_app.senpy.evaluate(params)
|
||||
return response
|
||||
|
||||
|
||||
@api_blueprint.route('/plugins/', methods=['POST', 'GET'])
|
||||
@basic_api
|
||||
def plugins():
|
||||
@@ -150,3 +224,12 @@ def plugins():
|
||||
def plugin(plugin=None):
|
||||
sp = current_app.senpy
|
||||
return sp.get_plugin(plugin)
|
||||
|
||||
|
||||
@api_blueprint.route('/datasets/', methods=['POST', 'GET'])
|
||||
@basic_api
|
||||
def datasets():
|
||||
sp = current_app.senpy
|
||||
datasets = sp.datasets
|
||||
dic = Datasets(datasets=list(datasets.values()))
|
||||
return dic
|
||||
|
@@ -12,12 +12,19 @@ class Client(object):
|
||||
def analyse(self, input, method='GET', **kwargs):
|
||||
return self.request('/', method=method, input=input, **kwargs)
|
||||
|
||||
def evaluate(self, input, method='GET', **kwargs):
|
||||
return self.request('/evaluate', method=method, input=input, **kwargs)
|
||||
|
||||
def plugins(self, *args, **kwargs):
|
||||
resp = self.request(path='/plugins').plugins
|
||||
return {p.name: p for p in resp}
|
||||
|
||||
def datasets(self):
|
||||
resp = self.request(path='/datasets').datasets
|
||||
return {d.name: d for d in resp}
|
||||
|
||||
def request(self, path=None, method='GET', **params):
|
||||
url = '{}{}'.format(self.endpoint, path)
|
||||
url = '{}{}'.format(self.endpoint.rstrip('/'), path)
|
||||
response = requests.request(method=method, url=url, params=params)
|
||||
try:
|
||||
resp = models.from_dict(response.json())
|
||||
|
@@ -6,8 +6,8 @@ from future import standard_library
|
||||
standard_library.install_aliases()
|
||||
|
||||
from . import plugins, api
|
||||
from .plugins import Plugin
|
||||
from .models import Error
|
||||
from .plugins import Plugin, evaluate
|
||||
from .models import Error, AggregatedEvaluation
|
||||
from .blueprints import api_blueprint, demo_blueprint, ns_blueprint
|
||||
|
||||
from threading import Thread
|
||||
@@ -16,14 +16,15 @@ import os
|
||||
import copy
|
||||
import errno
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
|
||||
from . import gsitk_compat
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Senpy(object):
|
||||
""" Default Senpy extension for Flask """
|
||||
|
||||
def __init__(self,
|
||||
app=None,
|
||||
plugin_folder=".",
|
||||
@@ -89,7 +90,7 @@ class Senpy(object):
|
||||
if plugin in self._plugins:
|
||||
return self._plugins[plugin]
|
||||
|
||||
results = self.plugins(id='plugins/{}'.format(name))
|
||||
results = self.plugins(id='endpoint:plugins/{}'.format(name))
|
||||
|
||||
if not results:
|
||||
return Error(message="Plugin not found", status=404)
|
||||
@@ -161,8 +162,7 @@ class Senpy(object):
|
||||
yield i
|
||||
|
||||
def install_deps(self):
|
||||
for plugin in self.plugins(is_activated=True):
|
||||
plugins.install_deps(plugin)
|
||||
plugins.install_deps(*self.plugins())
|
||||
|
||||
def analyse(self, request):
|
||||
"""
|
||||
@@ -171,7 +171,6 @@ class Senpy(object):
|
||||
by api.parse_call().
|
||||
"""
|
||||
logger.debug("analysing request: {}".format(request))
|
||||
try:
|
||||
entries = request.entries
|
||||
request.entries = []
|
||||
plugins = self._get_plugins(request)
|
||||
@@ -180,16 +179,54 @@ class Senpy(object):
|
||||
results.entries.append(i)
|
||||
self.convert_emotions(results)
|
||||
logger.debug("Returning analysis result: {}".format(results))
|
||||
except (Error, Exception) as ex:
|
||||
if not isinstance(ex, Error):
|
||||
msg = "Error during analysis: {} \n\t{}".format(ex,
|
||||
traceback.format_exc())
|
||||
ex = Error(message=msg, status=500)
|
||||
logger.exception('Error returning analysis result')
|
||||
raise ex
|
||||
results.analysis = [i['plugin'].id for i in results.analysis]
|
||||
return results
|
||||
|
||||
def _get_datasets(self, request):
|
||||
if not self.datasets:
|
||||
raise Error(
|
||||
status=404,
|
||||
message=("No datasets found."
|
||||
" Please verify DatasetManager"))
|
||||
datasets_name = request.parameters.get('dataset', None).split(',')
|
||||
for dataset in datasets_name:
|
||||
if dataset not in self.datasets:
|
||||
logger.debug(("The dataset '{}' is not valid\n"
|
||||
"Valid datasets: {}").format(dataset,
|
||||
self.datasets.keys()))
|
||||
raise Error(
|
||||
status=404,
|
||||
message="The dataset '{}' is not valid".format(dataset))
|
||||
dm = gsitk_compat.DatasetManager()
|
||||
datasets = dm.prepare_datasets(datasets_name)
|
||||
return datasets
|
||||
|
||||
@property
|
||||
def datasets(self):
|
||||
self._dataset_list = {}
|
||||
dm = gsitk_compat.DatasetManager()
|
||||
for item in dm.get_datasets():
|
||||
for key in item:
|
||||
if key in self._dataset_list:
|
||||
continue
|
||||
properties = item[key]
|
||||
properties['@id'] = key
|
||||
self._dataset_list[key] = properties
|
||||
return self._dataset_list
|
||||
|
||||
def evaluate(self, params):
|
||||
logger.debug("evaluating request: {}".format(params))
|
||||
results = AggregatedEvaluation()
|
||||
results.parameters = params
|
||||
datasets = self._get_datasets(results)
|
||||
plugins = self._get_plugins(results)
|
||||
for eval in evaluate(plugins, datasets):
|
||||
results.evaluations.append(eval)
|
||||
if 'with_parameters' not in results.parameters:
|
||||
del results.parameters
|
||||
logger.debug("Returning evaluation result: {}".format(results))
|
||||
return results
|
||||
|
||||
def _conversion_candidates(self, fromModel, toModel):
|
||||
candidates = self.plugins(plugin_type='emotionConversionPlugin')
|
||||
for candidate in candidates:
|
||||
@@ -271,10 +308,15 @@ class Senpy(object):
|
||||
else:
|
||||
self._default = self._plugins[value.lower()]
|
||||
|
||||
def activate_all(self, sync=True):
|
||||
def activate_all(self, sync=True, allow_fail=False):
|
||||
ps = []
|
||||
for plug in self._plugins.keys():
|
||||
ps.append(self.activate_plugin(plug, sync=sync))
|
||||
try:
|
||||
self.activate_plugin(plug, sync=sync)
|
||||
except Exception as ex:
|
||||
if not allow_fail:
|
||||
raise
|
||||
logger.error('Could not activate {}: {}'.format(plug, ex))
|
||||
return ps
|
||||
|
||||
def deactivate_all(self, sync=True):
|
||||
@@ -299,6 +341,7 @@ class Senpy(object):
|
||||
logger.info(msg)
|
||||
success = True
|
||||
self._set_active(plugin, success)
|
||||
return success
|
||||
|
||||
def activate_plugin(self, plugin_name, sync=True):
|
||||
plugin_name = plugin_name.lower()
|
||||
@@ -310,7 +353,7 @@ class Senpy(object):
|
||||
logger.info("Activating plugin: {}".format(plugin.name))
|
||||
|
||||
if sync or 'async' in plugin and not plugin.async:
|
||||
self._activate(plugin)
|
||||
return self._activate(plugin)
|
||||
else:
|
||||
th = Thread(target=partial(self._activate, plugin))
|
||||
th.start()
|
||||
|
23
senpy/gsitk_compat.py
Normal file
23
senpy/gsitk_compat.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MSG = 'GSITK is not (properly) installed.'
|
||||
IMPORTMSG = '{} Some functions will be unavailable.'.format(MSG)
|
||||
RUNMSG = '{} Install it to use this function.'.format(MSG)
|
||||
|
||||
|
||||
def raise_exception(*args, **kwargs):
|
||||
raise Exception(RUNMSG)
|
||||
|
||||
|
||||
try:
|
||||
from gsitk.datasets.datasets import DatasetManager
|
||||
from gsitk.evaluation.evaluation import Evaluation as Eval
|
||||
from sklearn.pipeline import Pipeline
|
||||
GSITK_AVAILABLE = True
|
||||
modules = locals()
|
||||
except ImportError:
|
||||
logger.warn(IMPORTMSG)
|
||||
GSITK_AVAILABLE = False
|
||||
DatasetManager = Eval = Pipeline = raise_exception
|
@@ -51,6 +51,10 @@ def read_schema(schema_file, absolute=False):
|
||||
return jsonref.load(f, base_uri=schema_uri)
|
||||
|
||||
|
||||
def dump_schema(schema):
|
||||
return jsonref.dumps(schema)
|
||||
|
||||
|
||||
def load_context(context):
|
||||
logging.debug('Loading context: {}'.format(context))
|
||||
if not context:
|
||||
@@ -134,7 +138,7 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
||||
@property
|
||||
def id(self):
|
||||
if '@id' not in self:
|
||||
self['@id'] = ':{}_{}'.format(type(self).__name__, time.time())
|
||||
self['@id'] = '_:{}_{}'.format(type(self).__name__, time.time())
|
||||
return self['@id']
|
||||
|
||||
@id.setter
|
||||
@@ -142,7 +146,7 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
||||
self['@id'] = value
|
||||
|
||||
def flask(self,
|
||||
in_headers=True,
|
||||
in_headers=False,
|
||||
headers=None,
|
||||
outformat='json-ld',
|
||||
**kwargs):
|
||||
@@ -172,20 +176,22 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
||||
|
||||
def serialize(self, format='json-ld', with_mime=False, **kwargs):
|
||||
js = self.jsonld(**kwargs)
|
||||
content = json.dumps(js, indent=2, sort_keys=True)
|
||||
if format == 'json-ld':
|
||||
content = json.dumps(js, indent=2, sort_keys=True)
|
||||
mimetype = "application/json"
|
||||
elif format in ['turtle', ]:
|
||||
elif format in ['turtle', 'ntriples']:
|
||||
logger.debug(js)
|
||||
content = json.dumps(js, indent=2, sort_keys=True)
|
||||
base = kwargs.get('prefix')
|
||||
g = Graph().parse(
|
||||
data=content,
|
||||
format='json-ld',
|
||||
base=kwargs.get('prefix'),
|
||||
context=self._context)
|
||||
base=base,
|
||||
context=[self._context,
|
||||
{'@base': base}])
|
||||
logger.debug(
|
||||
'Parsing with prefix: {}'.format(kwargs.get('prefix')))
|
||||
content = g.serialize(format='turtle').decode('utf-8')
|
||||
content = g.serialize(format=format,
|
||||
base=base).decode('utf-8')
|
||||
mimetype = 'text/{}'.format(format)
|
||||
else:
|
||||
raise Error('Unknown outformat: {}'.format(format))
|
||||
@@ -199,24 +205,23 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
||||
context_uri=None,
|
||||
prefix=None,
|
||||
expanded=False):
|
||||
ser = self.serializable()
|
||||
|
||||
result = jsonld.compact(
|
||||
ser,
|
||||
self._context,
|
||||
options={
|
||||
'base': prefix,
|
||||
'expandContext': self._context,
|
||||
'senpy': prefix
|
||||
})
|
||||
if context_uri:
|
||||
result['@context'] = context_uri
|
||||
result = self.serializable()
|
||||
|
||||
if expanded:
|
||||
result = jsonld.expand(
|
||||
result, options={'base': prefix,
|
||||
'expandContext': self._context})
|
||||
'expandContext': self._context})[0]
|
||||
if not with_context:
|
||||
try:
|
||||
del result['@context']
|
||||
except KeyError:
|
||||
pass
|
||||
elif context_uri:
|
||||
result['@context'] = context_uri
|
||||
else:
|
||||
result['@context'] = self._context
|
||||
|
||||
return result
|
||||
|
||||
def validate(self, obj=None):
|
||||
@@ -319,7 +324,10 @@ def _add_class_from_schema(*args, **kwargs):
|
||||
|
||||
|
||||
for i in [
|
||||
'aggregatedEvaluation',
|
||||
'analysis',
|
||||
'dataset',
|
||||
'datasets',
|
||||
'emotion',
|
||||
'emotionConversion',
|
||||
'emotionConversionPlugin',
|
||||
@@ -327,12 +335,17 @@ for i in [
|
||||
'emotionModel',
|
||||
'emotionPlugin',
|
||||
'emotionSet',
|
||||
'evaluation',
|
||||
'entity',
|
||||
'help',
|
||||
'metric',
|
||||
'plugin',
|
||||
'plugins',
|
||||
'response',
|
||||
'results',
|
||||
'sentimentPlugin',
|
||||
'suggestion',
|
||||
'topic',
|
||||
|
||||
]:
|
||||
_add_class_from_schema(i)
|
||||
|
@@ -3,6 +3,7 @@ standard_library.install_aliases()
|
||||
|
||||
|
||||
from future.utils import with_metaclass
|
||||
from functools import partial
|
||||
|
||||
import os.path
|
||||
import os
|
||||
@@ -18,9 +19,13 @@ import subprocess
|
||||
import importlib
|
||||
import yaml
|
||||
import threading
|
||||
import nltk
|
||||
|
||||
from .. import models, utils
|
||||
from .. import api
|
||||
from .. import gsitk_compat
|
||||
from .. import testing
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -38,11 +43,11 @@ class PluginMeta(models.BaseMeta):
|
||||
attrs['name'] = alias
|
||||
if 'description' not in attrs:
|
||||
doc = attrs.get('__doc__', None)
|
||||
if not doc:
|
||||
raise Exception(('Please, add a description or '
|
||||
'documentation to class {}').format(name))
|
||||
if doc:
|
||||
attrs['description'] = doc
|
||||
attrs['name'] = alias
|
||||
else:
|
||||
logger.warn(('Plugin {} does not have a description. '
|
||||
'Please, add a short summary to help other developers').format(name))
|
||||
cls = super(PluginMeta, mcs).__new__(mcs, name, bases, attrs)
|
||||
|
||||
if alias in mcs._classes:
|
||||
@@ -82,10 +87,30 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
||||
if info:
|
||||
self.update(info)
|
||||
self.validate()
|
||||
self.id = 'plugins/{}_{}'.format(self['name'], self['version'])
|
||||
self.id = 'endpoint:plugins/{}_{}'.format(self['name'], self['version'])
|
||||
self.is_activated = False
|
||||
self._lock = threading.Lock()
|
||||
self.data_folder = data_folder or os.getcwd()
|
||||
self._directory = os.path.abspath(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):
|
||||
missing = []
|
||||
@@ -114,19 +139,28 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
||||
for case in test_cases:
|
||||
try:
|
||||
self.test_case(case)
|
||||
logger.debug('Test case passed:\n{}'.format(pprint.pformat(case)))
|
||||
self.log.debug('Test case passed:\n{}'.format(pprint.pformat(case)))
|
||||
except Exception as ex:
|
||||
logger.warn('Test case failed:\n{}'.format(pprint.pformat(case)))
|
||||
self.log.warn('Test case failed:\n{}'.format(pprint.pformat(case)))
|
||||
raise
|
||||
|
||||
def test_case(self, case):
|
||||
def test_case(self, case, mock=testing.MOCK_REQUESTS):
|
||||
entry = models.Entry(case['entry'])
|
||||
given_parameters = case.get('params', case.get('parameters', {}))
|
||||
expected = case.get('expected', None)
|
||||
should_fail = case.get('should_fail', False)
|
||||
responses = case.get('responses', [])
|
||||
|
||||
try:
|
||||
params = api.parse_params(given_parameters, self.extra_params)
|
||||
res = list(self.analyse_entries([entry, ], params))
|
||||
|
||||
method = partial(self.analyse_entries, [entry, ], params)
|
||||
|
||||
if mock:
|
||||
res = list(method())
|
||||
else:
|
||||
with testing.patch_all_requests(responses):
|
||||
res = list(method())
|
||||
|
||||
if not isinstance(expected, list):
|
||||
expected = [expected]
|
||||
@@ -139,10 +173,22 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
||||
raise
|
||||
assert not should_fail
|
||||
|
||||
def open(self, fpath, *args, **kwargs):
|
||||
def find_file(self, fname):
|
||||
for p in self._data_paths:
|
||||
alternative = os.path.join(p, fname)
|
||||
if os.path.exists(alternative):
|
||||
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)
|
||||
return open(fpath, *args, **kwargs)
|
||||
else:
|
||||
fpath = self.find_file(fpath)
|
||||
|
||||
return open(fpath, mode=mode)
|
||||
|
||||
def serve(self, debug=True, **kwargs):
|
||||
utils.easy(plugin_list=[self, ], plugin_folder=None, debug=debug, **kwargs)
|
||||
@@ -177,7 +223,7 @@ class Analysis(Plugin):
|
||||
|
||||
def analyse_entries(self, entries, parameters):
|
||||
for entry in entries:
|
||||
logger.debug('Analysing entry with plugin {}: {}'.format(self, entry))
|
||||
self.log.debug('Analysing entry with plugin {}: {}'.format(self, entry))
|
||||
results = self.analyse_entry(entry, parameters)
|
||||
if inspect.isgenerator(results):
|
||||
for result in results:
|
||||
@@ -251,7 +297,7 @@ class Box(AnalysisPlugin):
|
||||
|
||||
.. code-block::
|
||||
|
||||
entry --> input() --> box() --> output() --> entry'
|
||||
entry --> input() --> predict_one() --> output() --> entry'
|
||||
|
||||
|
||||
In other words: their ``input`` method convers a query (entry and a set of parameters) into
|
||||
@@ -267,15 +313,33 @@ class Box(AnalysisPlugin):
|
||||
'''Transforms the results of the black box into an entry'''
|
||||
return output
|
||||
|
||||
def box(self):
|
||||
def predict_one(self, input):
|
||||
raise NotImplementedError('You should define the behavior of this plugin')
|
||||
|
||||
def analyse_entries(self, entries, params):
|
||||
for entry in entries:
|
||||
input = self.input(entry=entry, params=params)
|
||||
results = self.box(input=input, params=params)
|
||||
results = self.predict_one(input=input)
|
||||
yield self.output(output=results, entry=entry, params=params)
|
||||
|
||||
def fit(self, X=None, y=None):
|
||||
return self
|
||||
|
||||
def transform(self, X):
|
||||
return [self.predict_one(x) for x in X]
|
||||
|
||||
def predict(self, X):
|
||||
return self.transform(X)
|
||||
|
||||
def fit_transform(self, X, y):
|
||||
self.fit(X, y)
|
||||
return self.transform(X)
|
||||
|
||||
def as_pipe(self):
|
||||
pipe = gsitk_compat.Pipeline([('plugin', self)])
|
||||
pipe.name = self.name
|
||||
return pipe
|
||||
|
||||
|
||||
class TextBox(Box):
|
||||
'''A black box plugin that takes only text as input'''
|
||||
@@ -348,7 +412,7 @@ class ShelfMixin(object):
|
||||
with self.open(self.shelf_file, 'rb') as p:
|
||||
self._sh = pickle.load(p)
|
||||
except (IndexError, EOFError, pickle.UnpicklingError):
|
||||
logger.warning('{} has a corrupted shelf file!'.format(self.id))
|
||||
self.log.warning('Corrupted shelf file: {}'.format(self.shelf_file))
|
||||
if not self.get('force_shelf', False):
|
||||
raise
|
||||
return self._sh
|
||||
@@ -375,32 +439,31 @@ class ShelfMixin(object):
|
||||
self._shelf_file = value
|
||||
|
||||
def save(self):
|
||||
logger.debug('saving pickle')
|
||||
self.log.debug('Saving pickle')
|
||||
if hasattr(self, '_sh') and self._sh is not None:
|
||||
with self.open(self.shelf_file, 'wb') as f:
|
||||
pickle.dump(self._sh, f)
|
||||
|
||||
|
||||
def pfilter(plugins, **kwargs):
|
||||
def pfilter(plugins, plugin_type=Analysis, **kwargs):
|
||||
""" Filter plugins by different criteria """
|
||||
if isinstance(plugins, models.Plugins):
|
||||
plugins = plugins.plugins
|
||||
elif isinstance(plugins, dict):
|
||||
plugins = plugins.values()
|
||||
ptype = kwargs.pop('plugin_type', Plugin)
|
||||
logger.debug('#' * 100)
|
||||
logger.debug('ptype {}'.format(ptype))
|
||||
if ptype:
|
||||
if isinstance(ptype, PluginMeta):
|
||||
ptype = ptype.__name__
|
||||
logger.debug('plugin_type {}'.format(plugin_type))
|
||||
if plugin_type:
|
||||
if isinstance(plugin_type, PluginMeta):
|
||||
plugin_type = plugin_type.__name__
|
||||
try:
|
||||
ptype = ptype[0].upper() + ptype[1:]
|
||||
pclass = globals()[ptype]
|
||||
plugin_type = plugin_type[0].upper() + plugin_type[1:]
|
||||
pclass = globals()[plugin_type]
|
||||
logger.debug('Class: {}'.format(pclass))
|
||||
candidates = filter(lambda x: isinstance(x, pclass),
|
||||
plugins)
|
||||
except KeyError:
|
||||
raise models.Error('{} is not a valid type'.format(ptype))
|
||||
raise models.Error('{} is not a valid type'.format(plugin_type))
|
||||
else:
|
||||
candidates = plugins
|
||||
|
||||
@@ -435,10 +498,11 @@ def _log_subprocess_output(process):
|
||||
|
||||
def install_deps(*plugins):
|
||||
installed = False
|
||||
nltk_resources = set()
|
||||
for info in plugins:
|
||||
requirements = info.get('requirements', [])
|
||||
if requirements:
|
||||
pip_args = [sys.executable, '-m', 'pip', 'install', '--use-wheel']
|
||||
pip_args = [sys.executable, '-m', 'pip', 'install']
|
||||
for req in requirements:
|
||||
pip_args.append(req)
|
||||
logger.info('Installing requirements: ' + str(requirements))
|
||||
@@ -449,11 +513,15 @@ def install_deps(*plugins):
|
||||
exitcode = process.wait()
|
||||
installed = True
|
||||
if exitcode != 0:
|
||||
raise models.Error("Dependencies not properly installed")
|
||||
raise models.Error("Dependencies not properly installed: {}".format(pip_args))
|
||||
nltk_resources |= set(info.get('nltk_resources', []))
|
||||
|
||||
installed |= nltk.download(list(nltk_resources))
|
||||
return installed
|
||||
|
||||
|
||||
is_plugin_file = re.compile(r'.*\.senpy$|senpy_[a-zA-Z0-9_]+\.py$|[a-zA-Z0-9_]+_plugin.py$')
|
||||
is_plugin_file = re.compile(r'.*\.senpy$|senpy_[a-zA-Z0-9_]+\.py$|'
|
||||
'^(?!test_)[a-zA-Z0-9_]+_plugin.py$')
|
||||
|
||||
|
||||
def find_plugins(folders):
|
||||
@@ -466,7 +534,7 @@ def find_plugins(folders):
|
||||
yield fpath
|
||||
|
||||
|
||||
def from_path(fpath, **kwargs):
|
||||
def from_path(fpath, install_on_fail=False, **kwargs):
|
||||
logger.debug("Loading plugin from {}".format(fpath))
|
||||
if fpath.endswith('.py'):
|
||||
# We asume root is the dir of the file, and module is the name of the file
|
||||
@@ -476,7 +544,7 @@ def from_path(fpath, **kwargs):
|
||||
yield instance
|
||||
else:
|
||||
info = parse_plugin_info(fpath)
|
||||
yield from_info(info, **kwargs)
|
||||
yield from_info(info, install_on_fail=install_on_fail, **kwargs)
|
||||
|
||||
|
||||
def from_folder(folders, loader=from_path, **kwargs):
|
||||
@@ -487,7 +555,7 @@ def from_folder(folders, loader=from_path, **kwargs):
|
||||
return plugins
|
||||
|
||||
|
||||
def from_info(info, root=None, **kwargs):
|
||||
def from_info(info, root=None, install_on_fail=True, **kwargs):
|
||||
if any(x not in info for x in ('module',)):
|
||||
raise ValueError('Plugin info is not valid: {}'.format(info))
|
||||
module = info["module"]
|
||||
@@ -495,7 +563,12 @@ def from_info(info, root=None, **kwargs):
|
||||
if not root and '_path' in info:
|
||||
root = os.path.dirname(info['_path'])
|
||||
|
||||
return one_from_module(module, root=root, info=info, **kwargs)
|
||||
fun = partial(one_from_module, module, root=root, info=info, **kwargs)
|
||||
try:
|
||||
return fun()
|
||||
except (ImportError, LookupError):
|
||||
install_deps(info)
|
||||
return fun()
|
||||
|
||||
|
||||
def parse_plugin_info(fpath):
|
||||
@@ -542,13 +615,7 @@ def _instances_in_module(module):
|
||||
yield obj
|
||||
|
||||
|
||||
def _from_module_name(module, root, info=None, install=True, **kwargs):
|
||||
try:
|
||||
module = load_module(module, root)
|
||||
except ImportError:
|
||||
if not install or not info:
|
||||
raise
|
||||
install_deps(info)
|
||||
def _from_module_name(module, root, info=None, **kwargs):
|
||||
module = load_module(module, root)
|
||||
for plugin in _from_loaded_module(module=module, root=root, info=info, **kwargs):
|
||||
yield plugin
|
||||
@@ -559,3 +626,47 @@ def _from_loaded_module(module, info=None, **kwargs):
|
||||
yield cls(info=info, **kwargs)
|
||||
for instance in _instances_in_module(module):
|
||||
yield instance
|
||||
|
||||
|
||||
def evaluate(plugins, datasets, **kwargs):
|
||||
ev = gsitk_compat.Eval(tuples=None,
|
||||
datasets=datasets,
|
||||
pipelines=[plugin.as_pipe() for plugin in plugins])
|
||||
ev.evaluate()
|
||||
results = ev.results
|
||||
evaluations = evaluations_to_JSONLD(results, **kwargs)
|
||||
return evaluations
|
||||
|
||||
|
||||
def evaluations_to_JSONLD(results, flatten=False):
|
||||
'''
|
||||
Map the evaluation results to a JSONLD scheme
|
||||
'''
|
||||
|
||||
evaluations = list()
|
||||
metric_names = ['accuracy', 'precision_macro', 'recall_macro',
|
||||
'f1_macro', 'f1_weighted', 'f1_micro', 'f1_macro']
|
||||
|
||||
for index, row in results.iterrows():
|
||||
evaluation = models.Evaluation()
|
||||
if row.get('CV', True):
|
||||
evaluation['@type'] = ['StaticCV', 'Evaluation']
|
||||
evaluation.evaluatesOn = row['Dataset']
|
||||
evaluation.evaluates = row['Model']
|
||||
i = 0
|
||||
if flatten:
|
||||
metric = models.Metric()
|
||||
for name in metric_names:
|
||||
metric[name] = row[name]
|
||||
evaluation.metrics.append(metric)
|
||||
else:
|
||||
# We should probably discontinue this representation
|
||||
for name in metric_names:
|
||||
metric = models.Metric()
|
||||
metric['@id'] = 'Metric' + str(i)
|
||||
metric['@type'] = name.capitalize()
|
||||
metric.value = row[name]
|
||||
evaluation.metrics.append(metric)
|
||||
i += 1
|
||||
evaluations.append(evaluation)
|
||||
return evaluations
|
||||
|
@@ -1,19 +0,0 @@
|
||||
---
|
||||
name: split
|
||||
module: senpy.plugins.misc.split
|
||||
description: A sample plugin that chunks input text
|
||||
author: "@militarpancho"
|
||||
version: '0.2'
|
||||
url: "https://github.com/gsi-upm/senpy"
|
||||
requirements:
|
||||
- nltk
|
||||
extra_params:
|
||||
delimiter:
|
||||
aliases:
|
||||
- type
|
||||
- t
|
||||
required: false
|
||||
default: sentence
|
||||
options:
|
||||
- sentence
|
||||
- paragraph
|
@@ -5,13 +5,27 @@ from nltk.tokenize.simple import LineTokenizer
|
||||
import nltk
|
||||
|
||||
|
||||
class SplitPlugin(AnalysisPlugin):
|
||||
class Split(AnalysisPlugin):
|
||||
'''description: A sample plugin that chunks input text'''
|
||||
|
||||
author = ["@militarpancho", '@balkian']
|
||||
version = '0.2'
|
||||
url = "https://github.com/gsi-upm/senpy"
|
||||
|
||||
extra_params = {
|
||||
'delimiter': {
|
||||
'aliases': ['type', 't'],
|
||||
'required': False,
|
||||
'default': 'sentence',
|
||||
'options': ['sentence', 'paragraph']
|
||||
},
|
||||
}
|
||||
|
||||
def activate(self):
|
||||
nltk.download('punkt')
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
yield entry
|
||||
chunker_type = params["delimiter"]
|
||||
original_text = entry['nif:isString']
|
||||
if chunker_type == "sentence":
|
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: sentiment140
|
||||
module: sentiment140
|
||||
description: "Connects to the sentiment140 free API: http://sentiment140.com"
|
||||
author: "@balkian"
|
||||
version: '0.2'
|
||||
url: "https://github.com/gsi-upm/senpy-plugins-community"
|
||||
extra_params:
|
||||
language:
|
||||
"@id": lang_sentiment140
|
||||
aliases:
|
||||
- language
|
||||
- l
|
||||
required: false
|
||||
options:
|
||||
- es
|
||||
- en
|
||||
- auto
|
||||
default: auto
|
||||
requirements: {}
|
||||
maxPolarityValue: 1
|
||||
minPolarityValue: 0
|
@@ -4,12 +4,31 @@ import json
|
||||
from senpy.plugins import SentimentPlugin
|
||||
from senpy.models import Sentiment
|
||||
|
||||
ENDPOINT = 'http://www.sentiment140.com/api/bulkClassifyJson'
|
||||
|
||||
class Sentiment140Plugin(SentimentPlugin):
|
||||
|
||||
class Sentiment140(SentimentPlugin):
|
||||
'''Connects to the sentiment140 free API: http://sentiment140.com'''
|
||||
|
||||
author = "@balkian"
|
||||
version = '0.2'
|
||||
url = "https://github.com/gsi-upm/senpy-plugins-community"
|
||||
extra_params = {
|
||||
'language': {
|
||||
"@id": 'lang_sentiment140',
|
||||
'aliases': ['language', 'l'],
|
||||
'required': False,
|
||||
'default': 'auto',
|
||||
'options': ['es', 'en', 'auto']
|
||||
}
|
||||
}
|
||||
|
||||
maxPolarityValue = 1
|
||||
minPolarityValue = 0
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
lang = params["language"]
|
||||
res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson",
|
||||
res = requests.post(ENDPOINT,
|
||||
json.dumps({
|
||||
"language": lang,
|
||||
"data": [{
|
||||
@@ -31,23 +50,10 @@ class Sentiment140Plugin(SentimentPlugin):
|
||||
marl__hasPolarity=polarity,
|
||||
marl__polarityValue=polarity_value)
|
||||
sentiment.prov__wasGeneratedBy = self.id
|
||||
entry.sentiments = []
|
||||
entry.sentiments.append(sentiment)
|
||||
entry.language = lang
|
||||
yield entry
|
||||
|
||||
def test(self, *args, **kwargs):
|
||||
'''
|
||||
To avoid calling the sentiment140 API, we will mock the results
|
||||
from requests.
|
||||
'''
|
||||
from senpy.test import patch_requests
|
||||
expected = {"data": [{"polarity": 10}]}
|
||||
with patch_requests(expected) as (request, response):
|
||||
super(Sentiment140Plugin, self).test(*args, **kwargs)
|
||||
assert request.called
|
||||
assert response.json.called
|
||||
|
||||
test_cases = [
|
||||
{
|
||||
'entry': {
|
||||
@@ -61,6 +67,9 @@ class Sentiment140Plugin(SentimentPlugin):
|
||||
'marl:hasPolarity': 'marl:Positive',
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
'responses': [{'url': ENDPOINT,
|
||||
'method': 'POST',
|
||||
'json': {'data': [{'polarity': 4}]}}]
|
||||
}
|
||||
]
|
38
senpy/schemas/aggregatedEvaluation.json
Normal file
38
senpy/schemas/aggregatedEvaluation.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"allOf": [
|
||||
{"$ref": "response.json"},
|
||||
{
|
||||
"title": "AggregatedEvaluation",
|
||||
"description": "The results of the evaluation",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@context": {
|
||||
"$ref": "context.json"
|
||||
},
|
||||
"@type": {
|
||||
"default": "AggregatedEvaluation"
|
||||
},
|
||||
"@id": {
|
||||
"description": "ID of the aggregated evaluation",
|
||||
"type": "string"
|
||||
},
|
||||
"evaluations": {
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "evaluation.json"
|
||||
},{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
"required": ["@id", "evaluations"]
|
||||
}
|
||||
]
|
||||
}
|
@@ -10,8 +10,10 @@
|
||||
"wna": "http://www.gsi.dit.upm.es/ontologies/wnaffect/ns#",
|
||||
"emoml": "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/emotionml/ns#",
|
||||
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
||||
"fam": "http://vocab.fusepool.info/fam#",
|
||||
"topics": {
|
||||
"@id": "dc:subject"
|
||||
"@id": "nif:topic",
|
||||
"@container": "@set"
|
||||
},
|
||||
"entities": {
|
||||
"@id": "me:hasEntities"
|
||||
|
29
senpy/schemas/dataset.json
Normal file
29
senpy/schemas/dataset.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"name": "Dataset",
|
||||
"properties": {
|
||||
"@id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"compression": {
|
||||
"type": "string"
|
||||
},
|
||||
"expected_bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"filename": {
|
||||
"description": "Name of the dataset",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "Classifier or plugin evaluated",
|
||||
"type": "string"
|
||||
},
|
||||
"stats": {
|
||||
}
|
||||
},
|
||||
"required": ["@id"]
|
||||
}
|
18
senpy/schemas/datasets.json
Normal file
18
senpy/schemas/datasets.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"allOf": [
|
||||
{"$ref": "response.json"},
|
||||
{
|
||||
"required": ["datasets"],
|
||||
"properties": {
|
||||
"datasets": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "dataset.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@@ -41,5 +41,20 @@
|
||||
},
|
||||
"Response": {
|
||||
"$ref": "response.json"
|
||||
},
|
||||
"AggregatedEvaluation": {
|
||||
"$ref": "aggregatedEvaluation.json"
|
||||
},
|
||||
"Evaluation": {
|
||||
"$ref": "evaluation.json"
|
||||
},
|
||||
"Metric": {
|
||||
"$ref": "metric.json"
|
||||
},
|
||||
"Dataset": {
|
||||
"$ref": "dataset.json"
|
||||
},
|
||||
"Datasets": {
|
||||
"$ref": "datasets.json"
|
||||
}
|
||||
}
|
||||
|
28
senpy/schemas/evaluation.json
Normal file
28
senpy/schemas/evaluation.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"name": "Evaluation",
|
||||
"properties": {
|
||||
"@id": {
|
||||
"type": "string"
|
||||
},
|
||||
"@type": {
|
||||
"type": "array",
|
||||
"default": "Evaluation"
|
||||
|
||||
},
|
||||
"metrics": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "metric.json" },
|
||||
"default": []
|
||||
},
|
||||
"evaluatesOn": {
|
||||
"description": "Name of the dataset evaluated ",
|
||||
"type": "string"
|
||||
},
|
||||
"evaluates": {
|
||||
"description": "Classifier or plugin evaluated",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["@id", "metrics"]
|
||||
}
|
24
senpy/schemas/metric.json
Normal file
24
senpy/schemas/metric.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"properties": {
|
||||
"@id": {
|
||||
"type": "string"
|
||||
},
|
||||
"@type": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxValue": {
|
||||
"type": "number"
|
||||
},
|
||||
"minValue": {
|
||||
"type": "number"
|
||||
},
|
||||
"value": {
|
||||
"type": "number"
|
||||
},
|
||||
"deviation": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["@id"]
|
||||
}
|
@@ -167,3 +167,36 @@ textarea{
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
.collapsed .collapseicon {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.collapsed .expandicon {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.expandicon {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.collapseicon {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.loader {
|
||||
border: 6px solid #f3f3f3; /* Light grey */
|
||||
border-top: 6px solid blue;
|
||||
border-bottom: 6px solid blue;
|
||||
|
||||
border-radius: 50%;
|
||||
width: 3em;
|
||||
height: 3em;
|
||||
animation: spin 2s linear infinite;
|
||||
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ var plugins_params = default_params = {};
|
||||
var plugins = [];
|
||||
var defaultPlugin = {};
|
||||
var gplugins = {};
|
||||
var pipeline = [];
|
||||
|
||||
function replaceURLWithHTMLLinks(text) {
|
||||
console.log('Text: ' + text);
|
||||
@@ -30,7 +31,14 @@ function hashchanged(){
|
||||
|
||||
|
||||
function get_plugins(response){
|
||||
plugins = response.plugins;
|
||||
for(ix in response.plugins){
|
||||
plug = response.plugins[ix];
|
||||
plugins[plug.name] = plug;
|
||||
}
|
||||
}
|
||||
|
||||
function get_datasets(response){
|
||||
datasets = response.datasets
|
||||
}
|
||||
|
||||
function group_plugins(){
|
||||
@@ -77,9 +85,34 @@ function draw_plugins_selection(){
|
||||
}
|
||||
}
|
||||
html += "</optgroup>"
|
||||
document.getElementById('plugins').innerHTML = html;
|
||||
// Two elements with plugin class
|
||||
// One from the evaluate tab and another one from the analyse tab
|
||||
plugin_lists = document.getElementsByClassName('plugin')
|
||||
for (element in plugin_lists){
|
||||
plugin_lists[element].innerHTML = html;
|
||||
}
|
||||
draw_plugin_pipeline();
|
||||
}
|
||||
|
||||
function draw_plugin_pipeline(){
|
||||
var pipeHTML = "";
|
||||
console.log("Drawing pipeline: ", pipeline);
|
||||
for (ix in pipeline){
|
||||
plug = pipeline[ix];
|
||||
pipeHTML += '<span onclick="remove_plugin_pipeline(\'' + plug + '\')" class="btn btn-primary"><span ><i class="fa fa-minus"></i></span> ' + plug + '</span> <i class="fa fa-arrow-right"></i> ';
|
||||
}
|
||||
console.log(pipeHTML);
|
||||
$("#pipeline").html(pipeHTML);
|
||||
}
|
||||
|
||||
|
||||
function remove_plugin_pipeline(name){
|
||||
console.log("Removing plugin: ", name);
|
||||
var index = pipeline.indexOf(name);
|
||||
pipeline.splice(index, 1);
|
||||
draw_plugin_pipeline();
|
||||
|
||||
}
|
||||
function draw_plugins_list(){
|
||||
var availablePlugins = document.getElementById('availablePlugins');
|
||||
|
||||
@@ -98,15 +131,40 @@ function draw_plugins_list(){
|
||||
}
|
||||
}
|
||||
|
||||
function add_plugin_pipeline(){
|
||||
var selected = get_selected_plugin();
|
||||
pipeline.push(selected);
|
||||
console.log("Adding ", selected);
|
||||
draw_plugin_pipeline();
|
||||
}
|
||||
|
||||
function draw_datasets(){
|
||||
html = "";
|
||||
repeated_html = "<input class=\"checks-datasets\" type=\"checkbox\" value=\"";
|
||||
for (dataset in datasets){
|
||||
html += repeated_html+datasets[dataset]["@id"]+"\">"+datasets[dataset]["@id"];
|
||||
html += "<br>"
|
||||
}
|
||||
document.getElementById("datasets").innerHTML = html;
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
var response = JSON.parse($.ajax({type: "GET", url: "/api/plugins/" , async: false}).responseText);
|
||||
defaultPlugin= JSON.parse($.ajax({type: "GET", url: "/api/plugins/default" , async: false}).responseText);
|
||||
|
||||
get_plugins(response);
|
||||
get_default_parameters();
|
||||
|
||||
draw_plugins_list();
|
||||
draw_plugins_selection();
|
||||
draw_parameters();
|
||||
draw_plugin_description();
|
||||
|
||||
if (evaluation_enabled) {
|
||||
var response2 = JSON.parse($.ajax({type: "GET", url: "/api/datasets/" , async: false}).responseText);
|
||||
get_datasets(response2);
|
||||
draw_datasets();
|
||||
}
|
||||
|
||||
$(window).on('hashchange', hashchanged);
|
||||
hashchanged();
|
||||
@@ -123,17 +181,34 @@ function get_default_parameters(){
|
||||
|
||||
}
|
||||
|
||||
function get_selected_plugin(){
|
||||
return document.getElementsByClassName('plugin')[0].options[document.getElementsByClassName('plugin')[0].selectedIndex].value;
|
||||
}
|
||||
|
||||
function draw_default_parameters(){
|
||||
var basic_params = document.getElementById("basic_params");
|
||||
basic_params.innerHTML = params_div(default_params);
|
||||
}
|
||||
|
||||
function update_params(params, plug){
|
||||
ep = plugins_params[plug];
|
||||
for(k in ep){
|
||||
params[k] = ep[k];
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
function draw_extra_parameters(){
|
||||
var plugin = document.getElementById("plugins").options[document.getElementById("plugins").selectedIndex].value;
|
||||
var plugin = get_selected_plugin();
|
||||
get_parameters();
|
||||
|
||||
var extra_params = document.getElementById("extra_params");
|
||||
extra_params.innerHTML = params_div(plugins_params[plugin]);
|
||||
var params = {};
|
||||
for (sel in pipeline){
|
||||
update_params(params, pipeline[sel]);
|
||||
}
|
||||
update_params(params, plugin);
|
||||
extra_params.innerHTML = params_div(params);
|
||||
}
|
||||
|
||||
function draw_parameters(){
|
||||
@@ -240,13 +315,27 @@ function add_param(key, value){
|
||||
return "&"+key+"="+value;
|
||||
}
|
||||
|
||||
function get_pipeline_arg(){
|
||||
arg = "";
|
||||
for (ix in pipeline){
|
||||
arg = arg + pipeline[ix] + ",";
|
||||
}
|
||||
arg = arg + get_selected_plugin();
|
||||
return arg;
|
||||
}
|
||||
|
||||
|
||||
function load_JSON(){
|
||||
url = "/api";
|
||||
var container = document.getElementById('results');
|
||||
var rawcontainer = document.getElementById("jsonraw");
|
||||
rawcontainer.innerHTML = '';
|
||||
container.innerHTML = '';
|
||||
var plugin = document.getElementById("plugins").options[document.getElementById("plugins").selectedIndex].value;
|
||||
|
||||
var plugin = get_pipeline_arg();
|
||||
$(".loading").addClass("loader");
|
||||
$("#preview").hide();
|
||||
|
||||
var input = encodeURIComponent(document.getElementById("input").value);
|
||||
url += "?algo="+plugin+"&i="+input
|
||||
|
||||
@@ -256,25 +345,120 @@ function load_JSON(){
|
||||
url += add_param(key, params[key]);
|
||||
}
|
||||
|
||||
var response = $.ajax({type: "GET", url: url , async: false}).responseText;
|
||||
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
|
||||
|
||||
document.getElementById("input_request").innerHTML = "<a href='"+url+"'>"+url+"</a>"
|
||||
$.ajax({type: "GET", url: url}).always(function(response){
|
||||
document.getElementById("results-div").style.display = 'block';
|
||||
try {
|
||||
response = JSON.parse(response);
|
||||
if(typeof response=="object") {
|
||||
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();
|
||||
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>"
|
||||
|
||||
$(".loading").removeClass("loader");
|
||||
$("#preview").show();
|
||||
});
|
||||
}
|
||||
|
||||
function get_datasets_from_checkbox(){
|
||||
var checks = document.getElementsByClassName("checks-datasets");
|
||||
|
||||
datasets = "";
|
||||
for (var i = 0; i < checks.length; i++){
|
||||
if (checks[i].checked){
|
||||
datasets += checks[i].value + ",";
|
||||
}
|
||||
}
|
||||
datasets = datasets.slice(0, -1);
|
||||
}
|
||||
|
||||
|
||||
function create_body_metrics(evaluations){
|
||||
var new_tbody = document.createElement('tbody')
|
||||
var metric_html = ""
|
||||
for (var eval in evaluations){
|
||||
metric_html += "<tr><th>"+evaluations[eval].evaluates+"</th><th>"+evaluations[eval].evaluatesOn+"</th>";
|
||||
for (var metric in evaluations[eval].metrics){
|
||||
metric_html += "<th>"+parseFloat(evaluations[eval].metrics[metric].value.toFixed(4))+"</th>";
|
||||
}
|
||||
metric_html += "</tr>";
|
||||
}
|
||||
new_tbody.innerHTML = metric_html
|
||||
return new_tbody
|
||||
}
|
||||
|
||||
function evaluate_JSON(){
|
||||
|
||||
url = "/api/evaluate";
|
||||
|
||||
var container = document.getElementById('results_eval');
|
||||
var rawcontainer = document.getElementById('jsonraw_eval');
|
||||
var table = document.getElementById("eval_table");
|
||||
|
||||
rawcontainer.innerHTML = "";
|
||||
container.innerHTML = "";
|
||||
|
||||
var plugin = document.getElementsByClassName("plugin")[0].options[document.getElementsByClassName("plugin")[0].selectedIndex].value;
|
||||
|
||||
get_datasets_from_checkbox();
|
||||
|
||||
url += "?algo="+plugin+"&dataset="+datasets
|
||||
|
||||
$('#doevaluate').attr("disabled", true);
|
||||
$.ajax({type: "GET", url: url, dataType: 'json'}).always(function(resp) {
|
||||
$('#doevaluate').attr("disabled", false);
|
||||
response = resp.responseText;
|
||||
|
||||
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
|
||||
|
||||
document.getElementById("input_request_eval").innerHTML = "<a href='"+url+"'>"+url+"</a>"
|
||||
document.getElementById("evaluate-div").style.display = 'block';
|
||||
|
||||
try {
|
||||
response = JSON.parse(response);
|
||||
var options = {
|
||||
mode: 'view'
|
||||
};
|
||||
|
||||
//Control the single response results
|
||||
if (!(Array.isArray(response.evaluations))){
|
||||
response.evaluations = [response.evaluations]
|
||||
}
|
||||
|
||||
new_tbody = create_body_metrics(response.evaluations)
|
||||
table.replaceChild(new_tbody, table.lastElementChild)
|
||||
|
||||
var editor = new JSONEditor(container, options, response);
|
||||
editor.expandAll();
|
||||
// $('#results-div a[href="#viewer"]').tab('show');
|
||||
$('#evaluate-div a[href="#evaluate-table"]').click();
|
||||
// location.hash = 'raw';
|
||||
|
||||
|
||||
}
|
||||
catch(err){
|
||||
console.log("Error decoding JSON (got turtle?)");
|
||||
$('#results-div a[href="#raw"]').click();
|
||||
$('#evaluate-div a[href="#evaluate-raw"]').click();
|
||||
// location.hash = 'raw';
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function draw_plugin_description(){
|
||||
var plugin = plugins[get_selected_plugin()];
|
||||
$("#plugdescription").text(plugin.description);
|
||||
console.log(plugin);
|
||||
}
|
||||
|
||||
function plugin_selected(){
|
||||
draw_extra_parameters();
|
||||
draw_plugin_description();
|
||||
}
|
||||
|
@@ -5,6 +5,9 @@
|
||||
<title>Playground {{version}}</title>
|
||||
|
||||
</head>
|
||||
<script>
|
||||
this.evaluation_enabled = {% if evaluation %}true{%else %}false{%endif%};
|
||||
</script>
|
||||
<script src="static/js/jquery-2.1.1.min.js" ></script>
|
||||
<!--<script src="jquery.autosize.min.js"></script>-->
|
||||
<link rel="stylesheet" href="static/css/bootstrap.min.css">
|
||||
@@ -32,6 +35,10 @@
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" ><a class="active" href="#about">About</a></li>
|
||||
<li role="presentation"class="active"><a class="active" href="#test">Test it</a></li>
|
||||
{% if evaluation %}
|
||||
<li role="presentation"><a class="active" href="#evaluate">Evaluate Plugins</a></li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
@@ -54,10 +61,19 @@
|
||||
<ul>
|
||||
<li>List all available plugins: <a href="/api/plugins">/api/plugins</a></li>
|
||||
<li>Get information about the default plugin: <a href="/api/plugins/default">/api/plugins/default</a></li>
|
||||
<li>List all available datasets: <a href="/api/datasets">/api/datasets</a></li>
|
||||
<li>Download the JSON-LD context used: <a href="/api/contexts/Results.jsonld">/api/contexts/Results.jsonld</a></li>
|
||||
</ul>
|
||||
|
||||
</p>
|
||||
<p>Senpy is a research project. If you use it in your research, please cite:
|
||||
<pre>
|
||||
Senpy: A Pragmatic Linked Sentiment Analysis Framework.
|
||||
Sánchez-Rada, J. F., Iglesias, C. A., Corcuera, I., & Araque, Ó.
|
||||
In Data Science and Advanced Analytics (DSAA),
|
||||
2016 IEEE International Conference on (pp. 735-742). IEEE.
|
||||
</pre>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<div class="col-lg-6 ">
|
||||
@@ -67,8 +83,6 @@
|
||||
</div>
|
||||
<div class="panel-body"><ul id=availablePlugins></ul></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 ">
|
||||
<a href="http://senpy.readthedocs.io">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><i class="fa fa-book"></i> If you are new to senpy, you might want to read senpy's documentation</div>
|
||||
@@ -79,9 +93,6 @@
|
||||
<div class="panel-heading"><i class="fa fa-sign-in"></i> Feel free to follow us on GitHub</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><i class="fa fa-child"></i> Enjoy.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,17 +104,28 @@
|
||||
whilst this text makes me happy and surprised at the same time.
|
||||
I cannot believe it!</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label>Select the plugin:</label>
|
||||
<select id="plugins" name="plugins" onchange="draw_extra_parameters()">
|
||||
</select>
|
||||
</div>
|
||||
<!-- PARAMETERS -->
|
||||
<div class="panel-group" id="parameters">
|
||||
<div class="panel panel-default">
|
||||
<a data-toggle="collapse" class="deco-none" href="#basic_params">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
Select the plugin.
|
||||
</h4>
|
||||
</div>
|
||||
<div id="plugin_selection" class="panel-collapse panel-body">
|
||||
<span id="pipeline"></span>
|
||||
<select name="plugins" class="plugin" onchange="plugin_selected()">
|
||||
</select>
|
||||
<span onclick="add_plugin_pipeline()"><span class="btn"><i class="fa fa-plus" title="Add more plugins to the pipeline. Processing order is left to right. i.e. the results of the leftmost plugin will be used as input for the second leftmost, and so on."></i></span></span>
|
||||
<label class="help-block " id="plugdescription"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<a data-toggle="collapse" class="deco-none collapsed" href="#basic_params">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fa fa-chevron-right pull-left expandicon"></i>
|
||||
<i class="fa fa-chevron-down pull-left collapseicon"></i>
|
||||
Basic API parameters
|
||||
</h4>
|
||||
</div>
|
||||
@@ -115,6 +137,8 @@ I cannot believe it!</textarea>
|
||||
<a data-toggle="collapse" class="deco-none" href="#extra_params">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fa fa-chevron-right pull-left expandicon"></i>
|
||||
<i class="fa fa-chevron-down pull-left collapseicon"></i>
|
||||
Plugin extra parameters
|
||||
</h4>
|
||||
</div>
|
||||
@@ -126,6 +150,7 @@ I cannot believe it!</textarea>
|
||||
<!-- END PARAMETERS -->
|
||||
|
||||
<a id="preview" class="btn btn-lg btn-primary" onclick="load_JSON()">Analyse!</a>
|
||||
<div id="loading-results" class="loading"></div>
|
||||
<!--<button id="visualise" name="type" type="button">Visualise!</button>-->
|
||||
</form>
|
||||
</div>
|
||||
@@ -151,6 +176,73 @@ I cannot believe it!</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if evaluation %}
|
||||
|
||||
<div class="tab-pane" id="evaluate">
|
||||
<div class="well">
|
||||
<form id="form" class="container" onsubmit="return getPlugins();" accept-charset="utf-8">
|
||||
<div>
|
||||
<label>Select the plugin:</label>
|
||||
<select id="plugins-eval" name="plugins-eval" class=plugin onchange="draw_extra_parameters()">
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Select the datasets:</label>
|
||||
<div id="datasets" name="datasets" >
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<a id="doevaluate" class="btn btn-lg btn-primary" onclick="evaluate_JSON()">Evaluate Plugin!</a>
|
||||
<!--<button id="visualise" name="type" type="button">Visualise!</button>-->
|
||||
</form>
|
||||
</div>
|
||||
<span id="input_request_eval"></span>
|
||||
<div id="evaluate-div">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a data-toggle="tab" class="active" href="#evaluate-viewer">Viewer</a></li>
|
||||
<li role="presentation"><a data-toggle="tab" class="active" href="#evaluate-raw">Raw</a></li>
|
||||
<li role="presentation"><a data-toggle="tab" class="active" href="#evaluate-table">Table</a></li>
|
||||
</ul>
|
||||
<div class="tab-content" id="evaluate-container">
|
||||
|
||||
<div class="tab-pane active" id="evaluate-viewer">
|
||||
<div id="content">
|
||||
<pre id="results_eval" class="results_eval"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="evaluate-raw">
|
||||
<div id="content">
|
||||
<pre id="jsonraw_eval" class="results_eval"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="evaluate-table">
|
||||
<table id="eval_table" class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Plugin</th>
|
||||
<th>Dataset</th>
|
||||
<th>Accuracy</th>
|
||||
<th>Precision_macro</th>
|
||||
<th>Recall_macro</th>
|
||||
<th>F1_macro</th>
|
||||
<th>F1_weighted</th>
|
||||
<th>F1_micro</th>
|
||||
<th>F1</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<a href="http://www.gsi.dit.upm.es" target="_blank"><img class="center-block" src="static/img/gsi.png"/> </a>
|
||||
|
||||
</div>
|
||||
|
@@ -1,30 +0,0 @@
|
||||
try:
|
||||
from unittest.mock import patch, MagicMock
|
||||
except ImportError:
|
||||
from mock import patch, MagicMock
|
||||
|
||||
|
||||
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()
|
||||
data = json.dumps(value)
|
||||
|
||||
success.json.return_value = value
|
||||
success.data.return_value = data
|
||||
success.status_code = code
|
||||
|
||||
success.content = json.dumps(value)
|
||||
method_mocker = MagicMock()
|
||||
method_mocker.return_value = success
|
||||
with patch.multiple('requests', request=method_mocker,
|
||||
get=method_mocker, post=method_mocker):
|
||||
yield method_mocker, success
|
||||
assert method_mocker.called
|
31
senpy/testing.py
Normal file
31
senpy/testing.py
Normal file
@@ -0,0 +1,31 @@
|
||||
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])
|
@@ -1,6 +1,7 @@
|
||||
from . import models, __version__
|
||||
from collections import MutableMapping
|
||||
import pprint
|
||||
import pdb
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -32,8 +33,8 @@ def check_template(indict, template):
|
||||
if indict != template:
|
||||
raise models.Error(('Differences found.\n'
|
||||
'\tExpected: {}\n'
|
||||
'\tFound: {}').format(pprint.pformat(indict),
|
||||
pprint.pformat(template)))
|
||||
'\tFound: {}').format(pprint.pformat(template),
|
||||
pprint.pformat(indict)))
|
||||
|
||||
|
||||
def convert_dictionary(original, mappings):
|
||||
@@ -67,17 +68,23 @@ def easy_load(app=None, plugin_list=None, plugin_folder=None, **kwargs):
|
||||
return sp, app
|
||||
|
||||
|
||||
def easy_test(plugin_list=None):
|
||||
def easy_test(plugin_list=None, debug=True):
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
try:
|
||||
if not plugin_list:
|
||||
from . import plugins
|
||||
import __main__
|
||||
logger.info('Loading classes from {}'.format(__main__))
|
||||
from . import plugins
|
||||
plugin_list = plugins.from_module(__main__)
|
||||
for plug in plugin_list:
|
||||
plug.test()
|
||||
logger.info('The tests for {} passed!'.format(plug.name))
|
||||
logger.info('All tests passed!')
|
||||
plug.log.info('My 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):
|
||||
|
32
setup.py
32
setup.py
@@ -1,20 +1,20 @@
|
||||
import pip
|
||||
from setuptools import setup
|
||||
# parse_requirements() returns generator of pip.req.InstallRequirement objects
|
||||
from pip.req import parse_requirements
|
||||
from senpy import __version__
|
||||
|
||||
try:
|
||||
install_reqs = parse_requirements(
|
||||
"requirements.txt", session=pip.download.PipSession())
|
||||
test_reqs = parse_requirements(
|
||||
"test-requirements.txt", session=pip.download.PipSession())
|
||||
except AttributeError:
|
||||
install_reqs = parse_requirements("requirements.txt")
|
||||
test_reqs = parse_requirements("test-requirements.txt")
|
||||
with open('senpy/VERSION') as f:
|
||||
__version__ = f.read().strip()
|
||||
assert __version__
|
||||
|
||||
install_reqs = [str(ir.req) for ir in install_reqs]
|
||||
test_reqs = [str(ir.req) for ir in test_reqs]
|
||||
|
||||
def parse_requirements(filename):
|
||||
""" load requirements from a pip requirements file """
|
||||
with open(filename, 'r') as f:
|
||||
lineiter = list(line.strip() for line in f)
|
||||
return [line for line in lineiter if line and not line.startswith("#")]
|
||||
|
||||
|
||||
install_reqs = parse_requirements("requirements.txt")
|
||||
test_reqs = parse_requirements("test-requirements.txt")
|
||||
extra_reqs = parse_requirements("extra-requirements.txt")
|
||||
|
||||
|
||||
setup(
|
||||
@@ -35,9 +35,7 @@ setup(
|
||||
tests_require=test_reqs,
|
||||
setup_requires=['pytest-runner', ],
|
||||
extras_require={
|
||||
'evaluation': [
|
||||
'gsitk'
|
||||
]
|
||||
'evaluation': extra_reqs
|
||||
},
|
||||
include_package_data=True,
|
||||
entry_points={
|
||||
|
@@ -32,7 +32,7 @@ class APITest(TestCase):
|
||||
query = {}
|
||||
plug_params = {
|
||||
'hello': {
|
||||
'aliases': ['hello', 'hiya'],
|
||||
'aliases': ['hiya', 'hello'],
|
||||
'required': True
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,26 @@ class APITest(TestCase):
|
||||
assert 'hello' in p
|
||||
assert p['hello'] == 'dlrow'
|
||||
|
||||
def test_parameters2(self):
|
||||
in1 = {
|
||||
'meaningcloud-key': 5
|
||||
}
|
||||
in2 = {
|
||||
'apikey': 25
|
||||
}
|
||||
extra_params = {
|
||||
"apikey": {
|
||||
"aliases": [
|
||||
"apikey",
|
||||
"meaningcloud-key"
|
||||
],
|
||||
"required": True
|
||||
}
|
||||
}
|
||||
p1 = parse_params(in1, extra_params)
|
||||
p2 = parse_params(in2, extra_params)
|
||||
assert (p2['apikey'] / p1['apikey']) == 5
|
||||
|
||||
def test_default(self):
|
||||
spec = {
|
||||
'hello': {
|
||||
|
@@ -21,7 +21,6 @@ class BlueprintsTest(TestCase):
|
||||
def setUpClass(cls):
|
||||
"""Set up only once, and re-use in every individual test"""
|
||||
cls.app = Flask("test_extensions")
|
||||
cls.app.debug = False
|
||||
cls.client = cls.app.test_client()
|
||||
cls.senpy = Senpy(default_plugins=True)
|
||||
cls.senpy.init_app(cls.app)
|
||||
@@ -31,17 +30,21 @@ class BlueprintsTest(TestCase):
|
||||
cls.senpy.activate_plugin("DummyRequired", sync=True)
|
||||
cls.senpy.default_plugin = 'Dummy'
|
||||
|
||||
def setUp(self):
|
||||
self.app.config['TESTING'] = True # Tell Flask not to catch Exceptions
|
||||
|
||||
def assertCode(self, resp, code):
|
||||
self.assertEqual(resp.status_code, code)
|
||||
|
||||
def test_playground(self):
|
||||
resp = self.client.get("/")
|
||||
assert "main.js" in resp.data.decode()
|
||||
assert "main.js" in resp.get_data(as_text=True)
|
||||
|
||||
def test_home(self):
|
||||
"""
|
||||
Calling with no arguments should ask the user for more arguments
|
||||
"""
|
||||
self.app.config['TESTING'] = False # Errors are expected in this case
|
||||
resp = self.client.get("/api/")
|
||||
self.assertCode(resp, 400)
|
||||
js = parse_resp(resp)
|
||||
@@ -81,7 +84,7 @@ class BlueprintsTest(TestCase):
|
||||
Extra params that have a required argument that does not
|
||||
have a default should raise an error.
|
||||
"""
|
||||
self.app.debug = False
|
||||
self.app.config['TESTING'] = False # Errors are expected in this case
|
||||
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired")
|
||||
self.assertCode(resp, 400)
|
||||
js = parse_resp(resp)
|
||||
@@ -97,7 +100,7 @@ class BlueprintsTest(TestCase):
|
||||
The dummy plugin returns an empty response,\
|
||||
it should contain the context
|
||||
"""
|
||||
self.app.debug = False
|
||||
self.app.config['TESTING'] = False # Errors are expected in this case
|
||||
resp = self.client.get("/api/?i=My aloha mohame&algo=DOESNOTEXIST")
|
||||
self.assertCode(resp, 404)
|
||||
js = parse_resp(resp)
|
||||
@@ -139,7 +142,7 @@ class BlueprintsTest(TestCase):
|
||||
js = parse_resp(resp)
|
||||
logging.debug(js)
|
||||
assert "@id" in js
|
||||
assert js["@id"] == "plugins/Dummy_0.1"
|
||||
assert js["@id"] == "endpoint:plugins/Dummy_0.1"
|
||||
|
||||
def test_default(self):
|
||||
""" Show only one plugin"""
|
||||
@@ -148,7 +151,7 @@ class BlueprintsTest(TestCase):
|
||||
js = parse_resp(resp)
|
||||
logging.debug(js)
|
||||
assert "@id" in js
|
||||
assert js["@id"] == "plugins/Dummy_0.1"
|
||||
assert js["@id"] == "endpoint:plugins/Dummy_0.1"
|
||||
|
||||
def test_context(self):
|
||||
resp = self.client.get("/api/contexts/context.jsonld")
|
||||
@@ -172,5 +175,6 @@ class BlueprintsTest(TestCase):
|
||||
assert "help" in js["valid_parameters"]
|
||||
|
||||
def test_conversion(self):
|
||||
self.app.config['TESTING'] = False # Errors are expected in this case
|
||||
resp = self.client.get("/api/?input=hello&algo=emoRand&emotionModel=DOES NOT EXIST")
|
||||
self.assertCode(resp, 404)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from senpy.test import patch_requests
|
||||
from senpy.testing import patch_requests
|
||||
from senpy.client import Client
|
||||
from senpy.models import Results, Plugins, Error
|
||||
from senpy.plugins import AnalysisPlugin
|
||||
@@ -14,22 +14,15 @@ class ModelsTest(TestCase):
|
||||
def test_client(self):
|
||||
endpoint = 'http://dummy/'
|
||||
client = Client(endpoint)
|
||||
with patch_requests(Results()) as (request, response):
|
||||
with patch_requests('http://dummy/', Results()):
|
||||
resp = client.analyse('hello')
|
||||
assert isinstance(resp, Results)
|
||||
request.assert_called_with(
|
||||
url=endpoint + '/', method='GET', params={'input': 'hello'})
|
||||
with patch_requests(Error('Nothing')) as (request, response):
|
||||
with patch_requests('http://dummy/', Error('Nothing')):
|
||||
try:
|
||||
client.analyse(input='hello', algorithm='NONEXISTENT')
|
||||
raise Exception('Exceptions should be raised. This is not golang')
|
||||
except Error:
|
||||
pass
|
||||
request.assert_called_with(
|
||||
url=endpoint + '/',
|
||||
method='GET',
|
||||
params={'input': 'hello',
|
||||
'algorithm': 'NONEXISTENT'})
|
||||
|
||||
def test_plugins(self):
|
||||
endpoint = 'http://dummy/'
|
||||
@@ -37,11 +30,8 @@ class ModelsTest(TestCase):
|
||||
plugins = Plugins()
|
||||
p1 = AnalysisPlugin({'name': 'AnalysisP1', 'version': 0, 'description': 'No'})
|
||||
plugins.plugins = [p1, ]
|
||||
with patch_requests(plugins) as (request, response):
|
||||
with patch_requests('http://dummy/plugins', plugins):
|
||||
response = client.plugins()
|
||||
assert isinstance(response, dict)
|
||||
assert len(response) == 1
|
||||
assert "AnalysisP1" in response
|
||||
request.assert_called_with(
|
||||
url=endpoint + '/plugins', method='GET',
|
||||
params={})
|
||||
|
@@ -47,7 +47,7 @@ class ExtensionsTest(TestCase):
|
||||
|
||||
def test_add_delete(self):
|
||||
'''Should be able to add and delete new plugins. '''
|
||||
new = plugins.Plugin(name='new', description='new', version=0)
|
||||
new = plugins.Analysis(name='new', description='new', version=0)
|
||||
self.senpy.add_plugin(new)
|
||||
assert new in self.senpy.plugins()
|
||||
self.senpy.delete_plugin(new)
|
||||
@@ -121,8 +121,8 @@ class ExtensionsTest(TestCase):
|
||||
# Leaf (defaultdict with __setattr__ and __getattr__.
|
||||
r1 = analyse(self.senpy, algorithm="Dummy", input="tupni", output="tuptuo")
|
||||
r2 = analyse(self.senpy, input="tupni", output="tuptuo")
|
||||
assert r1.analysis[0] == "plugins/Dummy_0.1"
|
||||
assert r2.analysis[0] == "plugins/Dummy_0.1"
|
||||
assert r1.analysis[0] == "endpoint:plugins/Dummy_0.1"
|
||||
assert r2.analysis[0] == "endpoint:plugins/Dummy_0.1"
|
||||
assert r1.entries[0]['nif:isString'] == 'input'
|
||||
|
||||
def test_analyse_empty(self):
|
||||
@@ -156,8 +156,8 @@ class ExtensionsTest(TestCase):
|
||||
r2 = analyse(self.senpy,
|
||||
input="tupni",
|
||||
output="tuptuo")
|
||||
assert r1.analysis[0] == "plugins/Dummy_0.1"
|
||||
assert r2.analysis[0] == "plugins/Dummy_0.1"
|
||||
assert r1.analysis[0] == "endpoint:plugins/Dummy_0.1"
|
||||
assert r2.analysis[0] == "endpoint:plugins/Dummy_0.1"
|
||||
assert r1.entries[0]['nif:isString'] == 'input'
|
||||
|
||||
def test_analyse_error(self):
|
||||
@@ -182,8 +182,7 @@ class ExtensionsTest(TestCase):
|
||||
analyse(self.senpy, input='nothing', algorithm='MOCK')
|
||||
assert False
|
||||
except Exception as ex:
|
||||
assert 'generic exception on analysis' in ex['message']
|
||||
assert ex['status'] == 500
|
||||
assert 'generic exception on analysis' in str(ex)
|
||||
|
||||
def test_filtering(self):
|
||||
""" Filtering plugins """
|
||||
|
@@ -9,6 +9,7 @@ from senpy.models import (Emotion,
|
||||
EmotionAnalysis,
|
||||
EmotionSet,
|
||||
Entry,
|
||||
Entity,
|
||||
Error,
|
||||
Results,
|
||||
Sentiment,
|
||||
@@ -207,3 +208,14 @@ class ModelsTest(TestCase):
|
||||
recovered = from_string(string)
|
||||
assert isinstance(recovered, Results)
|
||||
assert isinstance(recovered.entries[0], Entry)
|
||||
|
||||
def test_serializable(self):
|
||||
r = Results()
|
||||
e = Entry()
|
||||
ent = Entity()
|
||||
e.entities.append(ent)
|
||||
r.entries.append(e)
|
||||
d = r.serializable()
|
||||
assert d
|
||||
assert d['entries']
|
||||
assert d['entries'][0]['entities']
|
||||
|
@@ -1,6 +1,7 @@
|
||||
#!/bin/env python
|
||||
|
||||
import os
|
||||
import sys
|
||||
import pickle
|
||||
import shutil
|
||||
import tempfile
|
||||
@@ -10,6 +11,8 @@ from senpy.models import Results, Entry, EmotionSet, Emotion, Plugins
|
||||
from senpy import plugins
|
||||
from senpy.plugins.conversion.emotion.centroids import CentroidConversion
|
||||
|
||||
import pandas as pd
|
||||
|
||||
|
||||
class ShelfDummyPlugin(plugins.SentimentPlugin, plugins.ShelfMixin):
|
||||
'''Dummy plugin for tests.'''
|
||||
@@ -212,7 +215,7 @@ class PluginsTest(TestCase):
|
||||
def input(self, entry, **kwargs):
|
||||
return entry.text
|
||||
|
||||
def box(self, input, **kwargs):
|
||||
def predict_one(self, input):
|
||||
return 'SIGN' in input
|
||||
|
||||
def output(self, output, entry, **kwargs):
|
||||
@@ -242,7 +245,7 @@ class PluginsTest(TestCase):
|
||||
|
||||
mappings = {'happy': 'marl:Positive', 'sad': 'marl:Negative'}
|
||||
|
||||
def box(self, input, **kwargs):
|
||||
def predict_one(self, input, **kwargs):
|
||||
return 'happy' if ':)' in input else 'sad'
|
||||
|
||||
test_cases = [
|
||||
@@ -309,6 +312,48 @@ class PluginsTest(TestCase):
|
||||
res = c._backwards_conversion(e)
|
||||
assert res["onyx:hasEmotionCategory"] == "c2"
|
||||
|
||||
def _test_evaluation(self):
|
||||
testdata = []
|
||||
for i in range(50):
|
||||
testdata.append(["good", 1])
|
||||
for i in range(50):
|
||||
testdata.append(["bad", 0])
|
||||
dataset = pd.DataFrame(testdata, columns=['text', 'polarity'])
|
||||
|
||||
class DummyPlugin(plugins.TextBox):
|
||||
description = 'Plugin to test evaluation'
|
||||
version = 0
|
||||
|
||||
def predict_one(self, input):
|
||||
return 0
|
||||
|
||||
class SmartPlugin(plugins.TextBox):
|
||||
description = 'Plugin to test evaluation'
|
||||
version = 0
|
||||
|
||||
def predict_one(self, input):
|
||||
if input == 'good':
|
||||
return 1
|
||||
return 0
|
||||
|
||||
dpipe = DummyPlugin()
|
||||
results = plugins.evaluate(datasets={'testdata': dataset}, plugins=[dpipe], flatten=True)
|
||||
dumb_metrics = results[0].metrics[0]
|
||||
assert abs(dumb_metrics['accuracy'] - 0.5) < 0.01
|
||||
|
||||
spipe = SmartPlugin()
|
||||
results = plugins.evaluate(datasets={'testdata': dataset}, plugins=[spipe], flatten=True)
|
||||
smart_metrics = results[0].metrics[0]
|
||||
assert abs(smart_metrics['accuracy'] - 1) < 0.01
|
||||
|
||||
def test_evaluation(self):
|
||||
if sys.version_info < (3, 0):
|
||||
with self.assertRaises(Exception) as context:
|
||||
self._test_evaluation()
|
||||
self.assertTrue('GSITK ' in str(context.exception))
|
||||
else:
|
||||
self._test_evaluation()
|
||||
|
||||
|
||||
def make_mini_test(fpath):
|
||||
def mini_test(self):
|
||||
|
@@ -8,6 +8,8 @@ from fnmatch import fnmatch
|
||||
|
||||
from jsonschema import RefResolver, Draft4Validator, ValidationError
|
||||
|
||||
from senpy.models import read_schema
|
||||
|
||||
root_path = path.join(path.dirname(path.realpath(__file__)), '..')
|
||||
schema_folder = path.join(root_path, 'senpy', 'schemas')
|
||||
examples_path = path.join(root_path, 'docs', 'examples')
|
||||
@@ -15,7 +17,8 @@ bad_examples_path = path.join(root_path, 'docs', 'bad-examples')
|
||||
|
||||
|
||||
class JSONSchemaTests(unittest.TestCase):
|
||||
pass
|
||||
def test_definitions(self):
|
||||
read_schema('definitions.json')
|
||||
|
||||
|
||||
def do_create_(jsfile, success):
|
||||
|
33
tests/test_test.py
Normal file
33
tests/test_test.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from unittest import TestCase
|
||||
|
||||
import requests
|
||||
import json
|
||||
from senpy.testing import patch_requests
|
||||
from senpy.models import Results
|
||||
|
||||
ENDPOINT = 'http://example.com'
|
||||
|
||||
|
||||
class TestTest(TestCase):
|
||||
def test_patch_text(self):
|
||||
with patch_requests(ENDPOINT, 'hello'):
|
||||
r = requests.get(ENDPOINT)
|
||||
assert r.text == 'hello'
|
||||
|
||||
def test_patch_json(self):
|
||||
r = Results()
|
||||
with patch_requests(ENDPOINT, r):
|
||||
res = requests.get(ENDPOINT)
|
||||
assert res.text == json.dumps(r.jsonld())
|
||||
js = res.json()
|
||||
assert js
|
||||
assert js['@type'] == r['@type']
|
||||
|
||||
def test_patch_dict(self):
|
||||
r = {'nothing': 'new'}
|
||||
with patch_requests(ENDPOINT, r):
|
||||
res = requests.get(ENDPOINT)
|
||||
assert res.text == json.dumps(r)
|
||||
js = res.json()
|
||||
assert js
|
||||
assert js['nothing'] == 'new'
|
Reference in New Issue
Block a user