mirror of
https://github.com/gsi-upm/senpy
synced 2025-10-19 17:58:28 +00:00
Compare commits
51 Commits
0.9.0a5
...
44-add-bas
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
7c8dbf3262 | ||
|
41dc89b23b | ||
|
a951696317 | ||
|
1087692de2 | ||
|
3e2b8baeb2 | ||
|
21a5a3f201 | ||
|
abd401f863 | ||
|
bfc588a915 | ||
|
f93eed2cf5 | ||
|
0204e0b8e9 | ||
|
701f46b9f1 | ||
|
d1eca04eeb | ||
|
df7efbc57d | ||
|
89f3a0eca9 | ||
|
aa54d1c9c8 | ||
|
869c00f709 | ||
|
e329e84eef | ||
|
55be0e57da | ||
|
778746c5e8 | ||
|
19278d0acd | ||
|
694201d8d3 | ||
|
e8413fb645 | ||
|
390225df45 | ||
|
b03e03fd0a | ||
|
79e107bdcd | ||
|
c6e79fa50d |
@@ -55,6 +55,7 @@ push-latest:
|
|||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
- triggers
|
- triggers
|
||||||
|
- fix-makefiles
|
||||||
|
|
||||||
push-github:
|
push-github:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
@@ -69,6 +70,7 @@ deploy_pypi:
|
|||||||
stage: deploy
|
stage: deploy
|
||||||
script: # Configure the PyPI credentials, then push the package, and cleanup the creds.
|
script: # Configure the PyPI credentials, then push the package, and cleanup the creds.
|
||||||
- echo "[server-login]" >> ~/.pypirc
|
- echo "[server-login]" >> ~/.pypirc
|
||||||
|
- echo "repository=https://upload.pypi.org/legacy/" >> ~/.pypirc
|
||||||
- echo "username=" ${PYPI_USER} >> ~/.pypirc
|
- echo "username=" ${PYPI_USER} >> ~/.pypirc
|
||||||
- echo "password=" ${PYPI_PASSWORD} >> ~/.pypirc
|
- echo "password=" ${PYPI_PASSWORD} >> ~/.pypirc
|
||||||
- make pip_upload
|
- make pip_upload
|
||||||
|
@@ -2,18 +2,16 @@ export
|
|||||||
NAME ?= $(shell basename $(CURDIR))
|
NAME ?= $(shell basename $(CURDIR))
|
||||||
VERSION ?= $(shell git describe --tags --dirty 2>/dev/null)
|
VERSION ?= $(shell git describe --tags --dirty 2>/dev/null)
|
||||||
|
|
||||||
|
ifeq ($(VERSION),)
|
||||||
|
VERSION:=unknown
|
||||||
|
endif
|
||||||
|
|
||||||
# Get the location of this makefile.
|
# Get the location of this makefile.
|
||||||
MK_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
MK_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||||
|
|
||||||
-include .env
|
-include .env
|
||||||
-include ../.env
|
-include ../.env
|
||||||
|
|
||||||
.FORCE:
|
|
||||||
|
|
||||||
version: .FORCE
|
|
||||||
@echo $(VERSION) > $(NAME)/VERSION
|
|
||||||
@echo $(VERSION)
|
|
||||||
|
|
||||||
help: ## Show this help.
|
help: ## Show this help.
|
||||||
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/\(.*:\)[^#]*##\s*\(.*\)/\1\t\2/' | column -t -s " "
|
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/\(.*:\)[^#]*##\s*\(.*\)/\1\t\2/' | column -t -s " "
|
||||||
|
|
||||||
@@ -35,4 +33,4 @@ include $(MK_DIR)/git.mk
|
|||||||
info:: ## List all variables
|
info:: ## List all variables
|
||||||
env
|
env
|
||||||
|
|
||||||
.PHONY:: config help ci version .FORCE
|
.PHONY:: config help ci
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
IMAGEWTAG ?= $(IMAGENAME):$(VERSION)
|
IMAGENAME?=$(NAME)
|
||||||
|
IMAGEWTAG?=$(IMAGENAME):$(VERSION)
|
||||||
|
|
||||||
docker-login: ## Log in to the registry. It will only be used in the server, or when running a CI task locally (if CI_BUILD_TOKEN is set).
|
docker-login: ## Log in to the registry. It will only be used in the server, or when running a CI task locally (if CI_BUILD_TOKEN is set).
|
||||||
ifeq ($(CI_BUILD_TOKEN),)
|
ifeq ($(CI_BUILD_TOKEN),)
|
||||||
@@ -22,4 +23,7 @@ login:: docker-login
|
|||||||
|
|
||||||
clean:: docker-clean
|
clean:: docker-clean
|
||||||
|
|
||||||
|
docker-info:
|
||||||
|
@echo IMAGEWTAG=${IMAGEWTAG}
|
||||||
|
|
||||||
.PHONY:: docker-login docker-clean login clean
|
.PHONY:: docker-login docker-clean login clean
|
||||||
|
@@ -1,9 +1,17 @@
|
|||||||
PYVERSIONS ?= 2.7
|
PYVERSIONS ?= 3.5
|
||||||
PYMAIN ?= $(firstword $(PYVERSIONS))
|
PYMAIN ?= $(firstword $(PYVERSIONS))
|
||||||
TARNAME ?= $(NAME)-$(VERSION).tar.gz
|
TARNAME ?= $(NAME)-$(VERSION).tar.gz
|
||||||
|
VERSIONFILE ?= $(NAME)/VERSION
|
||||||
|
|
||||||
DEVPORT ?= 6000
|
DEVPORT ?= 6000
|
||||||
|
|
||||||
|
|
||||||
|
.FORCE:
|
||||||
|
|
||||||
|
version: .FORCE
|
||||||
|
@echo $(VERSION) > $(VERSIONFILE)
|
||||||
|
@echo $(VERSION)
|
||||||
|
|
||||||
yapf: ## Format python code
|
yapf: ## Format python code
|
||||||
yapf -i -r $(NAME)
|
yapf -i -r $(NAME)
|
||||||
yapf -i -r tests
|
yapf -i -r tests
|
||||||
@@ -20,7 +28,7 @@ quick_build: $(addprefix build-, $(PYMAIN))
|
|||||||
build: $(addprefix build-, $(PYVERSIONS)) ## Build all images / python versions
|
build: $(addprefix build-, $(PYVERSIONS)) ## Build all images / python versions
|
||||||
|
|
||||||
build-%: version Dockerfile-% ## Build a specific version (e.g. build-2.7)
|
build-%: version Dockerfile-% ## Build a specific version (e.g. build-2.7)
|
||||||
docker build -t '$(IMAGEWTAG)-python$*' --cache-from $(IMAGENAME):python$* -f Dockerfile-$* .;
|
docker build -t '$(IMAGEWTAG)-python$*' -f Dockerfile-$* .;
|
||||||
|
|
||||||
dev-%: ## Launch a specific development environment using docker (e.g. dev-2.7)
|
dev-%: ## Launch a specific development environment using docker (e.g. dev-2.7)
|
||||||
@docker start $(NAME)-dev$* || (\
|
@docker start $(NAME)-dev$* || (\
|
||||||
@@ -34,10 +42,10 @@ dev: dev-$(PYMAIN) ## Launch a development environment using docker, using the d
|
|||||||
|
|
||||||
quick_test: test-$(PYMAIN)
|
quick_test: test-$(PYMAIN)
|
||||||
|
|
||||||
test-%: ## Run setup.py from in an isolated container, built from the base image. (e.g. test-2.7)
|
test-%: build-% ## Run setup.py from in an isolated container, built from the base image. (e.g. test-2.7)
|
||||||
# This speeds tests up because the image has most (if not all) of the dependencies already.
|
# This speeds tests up because the image has most (if not all) of the dependencies already.
|
||||||
docker rm $(NAME)-test-$* || true
|
docker rm $(NAME)-test-$* || true
|
||||||
docker create -ti --name $(NAME)-test-$* --entrypoint="" -w /usr/src/app/ $(IMAGENAME):python$* python setup.py test
|
docker create -ti --name $(NAME)-test-$* --entrypoint="" -w /usr/src/app/ $(IMAGEWTAG)-python$* python setup.py test
|
||||||
docker cp . $(NAME)-test-$*:/usr/src/app
|
docker cp . $(NAME)-test-$*:/usr/src/app
|
||||||
docker start -a $(NAME)-test-$*
|
docker start -a $(NAME)-test-$*
|
||||||
|
|
||||||
@@ -89,4 +97,4 @@ clean:: ## Clean older docker images and containers related to this project and
|
|||||||
@docker ps -a | grep $(IMAGENAME) | awk '{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1;}}' | xargs docker rm -v 2>/dev/null|| true
|
@docker ps -a | grep $(IMAGENAME) | awk '{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1;}}' | xargs docker rm -v 2>/dev/null|| true
|
||||||
@docker images | grep $(IMAGENAME) | awk '{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1":"$$2;}}' | xargs docker rmi 2>/dev/null|| true
|
@docker images | grep $(IMAGENAME) | awk '{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1":"$$2;}}' | xargs docker rmi 2>/dev/null|| true
|
||||||
|
|
||||||
.PHONY:: yapf dockerfiles Dockerfile-% quick_build build build-% dev-% quick-dev test quick_test push-latest push-latest-% push-% push
|
.PHONY:: yapf dockerfiles Dockerfile-% quick_build build build-% dev-% quick-dev test quick_test push-latest push-latest-% push-% push version .FORCE
|
||||||
|
@@ -2,6 +2,12 @@ from python:{{PYVERSION}}
|
|||||||
|
|
||||||
MAINTAINER J. Fernando Sánchez <jf.sanchez@upm.es>
|
MAINTAINER J. Fernando Sánchez <jf.sanchez@upm.es>
|
||||||
|
|
||||||
|
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/
|
RUN mkdir /cache/ /senpy-plugins /data/
|
||||||
|
|
||||||
VOLUME /data/
|
VOLUME /data/
|
||||||
@@ -14,9 +20,9 @@ ONBUILD WORKDIR /senpy-plugins/
|
|||||||
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY test-requirements.txt requirements.txt /usr/src/app/
|
COPY test-requirements.txt requirements.txt extra-requirements.txt /usr/src/app/
|
||||||
RUN pip install --use-wheel -r test-requirements.txt -r requirements.txt
|
RUN pip install --no-cache-dir -r test-requirements.txt -r requirements.txt -r extra-requirements.txt
|
||||||
COPY . /usr/src/app/
|
COPY . /usr/src/app/
|
||||||
RUN pip install --no-index --no-deps --editable .
|
RUN pip install --no-cache-dir --no-index --no-deps --editable .
|
||||||
|
|
||||||
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]
|
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
.. image:: img/header.png
|
.. image:: img/header.png
|
||||||
:height: 6em
|
:width: 100%
|
||||||
:target: http://demos.gsi.dit.upm.es/senpy
|
:target: http://demos.gsi.dit.upm.es/senpy
|
||||||
|
|
||||||
.. image:: https://travis-ci.org/gsi-upm/senpy.svg?branch=master
|
.. image:: https://travis-ci.org/gsi-upm/senpy.svg?branch=master
|
||||||
|
106
docs/SenpyClientUse.rst
Normal file
106
docs/SenpyClientUse.rst
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
|
||||||
|
Client
|
||||||
|
======
|
||||||
|
|
||||||
|
Demo Endpoint
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Import Client and send a request
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from senpy.client import Client
|
||||||
|
|
||||||
|
c = Client('http://latest.senpy.cluster.gsi.dit.upm.es/api')
|
||||||
|
r = c.analyse('I like Pizza', algorithm='sentiment140')
|
||||||
|
|
||||||
|
Print response
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
for entry in r.entries:
|
||||||
|
print('{} -> {}'.format(entry['text'], entry['sentiments'][0]['marl:hasPolarity']))
|
||||||
|
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
I like Pizza -> marl:Positive
|
||||||
|
|
||||||
|
|
||||||
|
Obtain a list of available plugins
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
for plugin in c.request('/plugins')['plugins']:
|
||||||
|
print(plugin['name'])
|
||||||
|
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
emoRand
|
||||||
|
rand
|
||||||
|
sentiment140
|
||||||
|
|
||||||
|
|
||||||
|
Local Endpoint
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Run a docker container with Senpy image and default plugins
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
docker run -ti --name 'SenpyEndpoint' -d -p 5000:5000 gsiupm/senpy:0.8.6 --host 0.0.0.0 --default-plugins
|
||||||
|
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
a0157cd98057072388bfebeed78a830da7cf0a796f4f1a3fd9188f9f2e5fe562
|
||||||
|
|
||||||
|
|
||||||
|
Import client and send a request to localhost
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
c_local = Client('http://127.0.0.1:5000/api')
|
||||||
|
r = c_local.analyse('Hello world', algorithm='sentiment140')
|
||||||
|
|
||||||
|
Print response
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
for entry in r.entries:
|
||||||
|
print('{} -> {}'.format(entry['text'], entry['sentiments'][0]['marl:hasPolarity']))
|
||||||
|
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
Hello world -> marl:Neutral
|
||||||
|
|
||||||
|
|
||||||
|
Obtain a list of available plugins deployed locally
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
c_local.plugins().keys()
|
||||||
|
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
rand
|
||||||
|
sentiment140
|
||||||
|
emoRand
|
||||||
|
|
||||||
|
|
||||||
|
Stop the docker container
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
!docker stop SenpyEndpoint
|
||||||
|
!docker rm SenpyEndpoint
|
||||||
|
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
SenpyEndpoint
|
||||||
|
SenpyEndpoint
|
||||||
|
|
113
docs/plugins-definition.rst
Normal file
113
docs/plugins-definition.rst
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
Advanced plugin definition
|
||||||
|
--------------------------
|
||||||
|
In addition to finding plugins defined in source code files, senpy can also load a special type of definition file (`.senpy` files).
|
||||||
|
This used to be the only mechanism for loading in earlier versions of senpy.
|
||||||
|
|
||||||
|
The definition file contains basic information
|
||||||
|
|
||||||
|
Lastly, it is also possible to add new plugins programmatically.
|
||||||
|
|
||||||
|
.. contents:: :local:
|
||||||
|
|
||||||
|
What is a plugin?
|
||||||
|
=================
|
||||||
|
|
||||||
|
A plugin is a program that, given a text, will add annotations to it.
|
||||||
|
In practice, a plugin consists of at least two files:
|
||||||
|
|
||||||
|
- Definition file: a `.senpy` file that describes the plugin (e.g. what input parameters it accepts, what emotion model it uses).
|
||||||
|
- Python module: the actual code that will add annotations to each input.
|
||||||
|
|
||||||
|
This separation allows us to deploy plugins that use the same code but employ different parameters.
|
||||||
|
For instance, one could use the same classifier and processing in several plugins, but train with different datasets.
|
||||||
|
This scenario is particularly useful for evaluation purposes.
|
||||||
|
|
||||||
|
The only limitation is that the name of each plugin needs to be unique.
|
||||||
|
|
||||||
|
Definition files
|
||||||
|
================
|
||||||
|
|
||||||
|
The definition file complements and overrides the attributes provided by the plugin.
|
||||||
|
It can be written in YAML or JSON.
|
||||||
|
The most important attributes are:
|
||||||
|
|
||||||
|
* **name**: unique name that senpy will use internally to identify the plugin.
|
||||||
|
* **module**: indicates the module that contains the plugin code, which will be automatically loaded by senpy.
|
||||||
|
* **version**
|
||||||
|
* extra_params: to add parameters to the senpy API when this plugin is requested. Those parameters may be required, and have aliased names. For instance:
|
||||||
|
|
||||||
|
.. code:: yaml
|
||||||
|
|
||||||
|
extra_params:
|
||||||
|
hello_param:
|
||||||
|
aliases: # required
|
||||||
|
- hello_param
|
||||||
|
- hello
|
||||||
|
required: true
|
||||||
|
default: Hi you
|
||||||
|
values:
|
||||||
|
- Hi you
|
||||||
|
- Hello y'all
|
||||||
|
- Howdy
|
||||||
|
|
||||||
|
A complete example:
|
||||||
|
|
||||||
|
.. code:: yaml
|
||||||
|
|
||||||
|
name: <Name of the plugin>
|
||||||
|
module: <Python file>
|
||||||
|
version: 0.1
|
||||||
|
|
||||||
|
And the json equivalent:
|
||||||
|
|
||||||
|
.. code:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "<Name of the plugin>",
|
||||||
|
"module": "<Python file>",
|
||||||
|
"version": "0.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example plugin with a definition file
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
In this section, we will implement a basic sentiment analysis plugin.
|
||||||
|
To determine the polarity of each entry, the plugin will compare the length of the string to a threshold.
|
||||||
|
This threshold will be included in the definition file.
|
||||||
|
|
||||||
|
The definition file would look like this:
|
||||||
|
|
||||||
|
.. code:: yaml
|
||||||
|
|
||||||
|
name: helloworld
|
||||||
|
module: helloworld
|
||||||
|
version: 0.0
|
||||||
|
threshold: 10
|
||||||
|
description: Hello World
|
||||||
|
|
||||||
|
Now, in a file named ``helloworld.py``:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
#!/bin/env python
|
||||||
|
#helloworld.py
|
||||||
|
|
||||||
|
from senpy import AnalysisPlugin
|
||||||
|
from senpy import Sentiment
|
||||||
|
|
||||||
|
|
||||||
|
class HelloWorld(AnalysisPlugin):
|
||||||
|
|
||||||
|
def analyse_entry(entry, params):
|
||||||
|
'''Basically do nothing with each entry'''
|
||||||
|
|
||||||
|
sentiment = Sentiment()
|
||||||
|
if len(entry.text) < self.threshold:
|
||||||
|
sentiment['marl:hasPolarity'] = 'marl:Positive'
|
||||||
|
else:
|
||||||
|
sentiment['marl:hasPolarity'] = 'marl:Negative'
|
||||||
|
entry.sentiments.append(sentiment)
|
||||||
|
yield entry
|
||||||
|
|
||||||
|
The complete code of the example plugin is available `here <https://lab.cluster.gsi.dit.upm.es/senpy/plugin-prueba>`__.
|
313
docs/plugins.rst
313
docs/plugins.rst
@@ -1,6 +1,8 @@
|
|||||||
Developing new plugins
|
Developing new plugins
|
||||||
----------------------
|
----------------------
|
||||||
This document describes how to develop a new analysis plugin. For an example of conversion plugins, see :doc:`conversion`.
|
This document contains the minimum to get you started with developing new analysis plugin.
|
||||||
|
For an example of conversion plugins, see :doc:`conversion`.
|
||||||
|
For a description of definition files, see :doc:`plugins-definition`.
|
||||||
|
|
||||||
A more step-by-step tutorial with slides is available `here <https://lab.cluster.gsi.dit.upm.es/senpy/senpy-tutorial>`__
|
A more step-by-step tutorial with slides is available `here <https://lab.cluster.gsi.dit.upm.es/senpy/senpy-tutorial>`__
|
||||||
|
|
||||||
@@ -9,83 +11,29 @@ A more step-by-step tutorial with slides is available `here <https://lab.cluster
|
|||||||
What is a plugin?
|
What is a plugin?
|
||||||
=================
|
=================
|
||||||
|
|
||||||
A plugin is a program that, given a text, will add annotations to it.
|
A plugin is a python object that can process entries. Given an entry, it will modify it, add annotations to it, or generate new entries.
|
||||||
In practice, a plugin consists of at least two files:
|
|
||||||
|
|
||||||
- Definition file: a `.senpy` file that describes the plugin (e.g. what input parameters it accepts, what emotion model it uses).
|
|
||||||
- Python module: the actual code that will add annotations to each input.
|
|
||||||
|
|
||||||
This separation allows us to deploy plugins that use the same code but employ different parameters.
|
|
||||||
For instance, one could use the same classifier and processing in several plugins, but train with different datasets.
|
|
||||||
This scenario is particularly useful for evaluation purposes.
|
|
||||||
|
|
||||||
The only limitation is that the name of each plugin needs to be unique.
|
|
||||||
|
|
||||||
Plugin Definition files
|
|
||||||
=======================
|
|
||||||
|
|
||||||
The definition file contains all the attributes of the plugin, and can be written in YAML or JSON.
|
|
||||||
When the server is launched, it will recursively search for definition files in the plugin folder (the current folder, by default).
|
|
||||||
The most important attributes are:
|
|
||||||
|
|
||||||
* **name**: unique name that senpy will use internally to identify the plugin.
|
|
||||||
* **module**: indicates the module that contains the plugin code, which will be automatically loaded by senpy.
|
|
||||||
* **version**
|
|
||||||
* extra_params: to add parameters to the senpy API when this plugin is requested. Those parameters may be required, and have aliased names. For instance:
|
|
||||||
|
|
||||||
.. code:: yaml
|
|
||||||
|
|
||||||
extra_params:
|
|
||||||
hello_param:
|
|
||||||
aliases: # required
|
|
||||||
- hello_param
|
|
||||||
- hello
|
|
||||||
required: true
|
|
||||||
default: Hi you
|
|
||||||
values:
|
|
||||||
- Hi you
|
|
||||||
- Hello y'all
|
|
||||||
- Howdy
|
|
||||||
|
|
||||||
Parameter validation will fail if a required parameter without a default has not been provided, or if the definition includes a set of values and the provided one does not match one of them.
|
|
||||||
|
|
||||||
|
|
||||||
A complete example:
|
What is an entry?
|
||||||
|
=================
|
||||||
.. code:: yaml
|
|
||||||
|
|
||||||
name: <Name of the plugin>
|
|
||||||
module: <Python file>
|
|
||||||
version: 0.1
|
|
||||||
|
|
||||||
And the json equivalent:
|
|
||||||
|
|
||||||
.. code:: json
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "<Name of the plugin>",
|
|
||||||
"module": "<Python file>",
|
|
||||||
"version": "0.1"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Plugins Code
|
|
||||||
============
|
|
||||||
|
|
||||||
The basic methods in a plugin are:
|
|
||||||
|
|
||||||
* __init__
|
|
||||||
* activate: used to load memory-hungry resources
|
|
||||||
* deactivate: used to free up resources
|
|
||||||
* analyse_entry: called in every user requests. It takes two parameters: ``Entry``, the entry object, and ``params``, the parameters supplied by the user. It should yield one or more ``Entry`` objects.
|
|
||||||
|
|
||||||
Plugins are loaded asynchronously, so don't worry if the activate method takes too long. The plugin will be marked as activated once it is finished executing the method.
|
|
||||||
|
|
||||||
Entries
|
|
||||||
=======
|
|
||||||
|
|
||||||
Entries are objects that can be annotated.
|
Entries are objects that can be annotated.
|
||||||
|
In general, they will be a piece of text.
|
||||||
By default, entries are `NIF contexts <http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core/nif-core.html>`_ represented in JSON-LD format.
|
By default, entries are `NIF contexts <http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core/nif-core.html>`_ represented in JSON-LD format.
|
||||||
|
It is a dictionary/JSON object that looks like this:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
{
|
||||||
|
"@id": "<unique identifier or blank node name>",
|
||||||
|
"nif:isString": "input text",
|
||||||
|
"sentiments": [ {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
],
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
Annotations are added to the object like this:
|
Annotations are added to the object like this:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
@@ -100,96 +48,111 @@ The value may be any valid JSON-LD dictionary.
|
|||||||
For simplicity, senpy includes a series of models by default in the ``senpy.models`` module.
|
For simplicity, senpy includes a series of models by default in the ``senpy.models`` module.
|
||||||
|
|
||||||
|
|
||||||
Example plugin
|
What are annotations?
|
||||||
==============
|
=====================
|
||||||
|
They are objects just like entries.
|
||||||
|
Senpy ships with several default annotations, including: ``Sentiment``, ``Emotion``, ``EmotionSet``...jk bb
|
||||||
|
|
||||||
In this section, we will implement a basic sentiment analysis plugin.
|
|
||||||
To determine the polarity of each entry, the plugin will compare the length of the string to a threshold.
|
|
||||||
This threshold will be included in the definition file.
|
|
||||||
|
|
||||||
The definition file would look like this:
|
What's a plugin made of?
|
||||||
|
========================
|
||||||
|
|
||||||
.. code:: yaml
|
When receiving a query, senpy selects what plugin or plugins should process each entry, and in what order.
|
||||||
|
It also makes sure the every entry and the parameters provided by the user meet the plugin requirements.
|
||||||
|
|
||||||
name: helloworld
|
Hence, two parts are necessary: 1) the code that will process the entry, and 2) some attributes and metadata that will tell senpy how to interact with the plugin.
|
||||||
module: helloworld
|
|
||||||
version: 0.0
|
|
||||||
threshold: 10
|
|
||||||
description: Hello World
|
|
||||||
|
|
||||||
Now, in a file named ``helloworld.py``:
|
In practice, this is what a plugin looks like, tests included:
|
||||||
|
|
||||||
|
|
||||||
|
.. literalinclude:: ../senpy/plugins/example/rand_plugin.py
|
||||||
|
:emphasize-lines: 5-11
|
||||||
|
:language: python
|
||||||
|
|
||||||
|
|
||||||
|
The lines highlighted contain some information about the plugin.
|
||||||
|
In particular, the following information is mandatory:
|
||||||
|
|
||||||
|
* A unique name for the class. In our example, Rand.
|
||||||
|
* The subclass/type of plugin. This is typically either `SentimentPlugin` or `EmotionPlugin`. However, new types of plugin can be created for different annotations. The only requirement is that these new types inherit from `senpy.Analysis`
|
||||||
|
* A description of the plugin. This can be done simply by adding a doc to the class.
|
||||||
|
* A version, which should get updated.
|
||||||
|
* An author name.
|
||||||
|
|
||||||
|
|
||||||
|
Plugins Code
|
||||||
|
============
|
||||||
|
|
||||||
|
The basic methods in a plugin are:
|
||||||
|
|
||||||
|
* analyse_entry: called in every user requests. It takes two parameters: ``Entry``, the entry object, and ``params``, the parameters supplied by the user. It should yield one or more ``Entry`` objects.
|
||||||
|
* activate: used to load memory-hungry resources. For instance, to train a classifier.
|
||||||
|
* deactivate: used to free up resources when the plugin is no longer needed.
|
||||||
|
|
||||||
|
Plugins are loaded asynchronously, so don't worry if the activate method takes too long. The plugin will be marked as activated once it is finished executing the method.
|
||||||
|
|
||||||
|
|
||||||
|
How does senpy find modules?
|
||||||
|
============================
|
||||||
|
|
||||||
|
Senpy looks for files of two types:
|
||||||
|
|
||||||
|
* Python files of the form `senpy_<NAME>.py` or `<NAME>_plugin.py`. In these files, it will look for: 1) Instances that inherit from `senpy.Plugin`, or subclasses of `senpy.Plugin` that can be initialized without a configuration file. i.e. classes that contain all the required attributes for a plugin.
|
||||||
|
* Plugin definition files (see :doc:`advanced-plugins`)
|
||||||
|
|
||||||
|
Defining additional parameters
|
||||||
|
==============================
|
||||||
|
|
||||||
|
Your plugin may ask for additional parameters from the users of the service by using the attribute ``extra_params`` in your plugin definition.
|
||||||
|
It takes a dictionary, where the keys are the name of the argument/parameter, and the value has the following fields:
|
||||||
|
|
||||||
|
* aliases: the different names which can be used in the request to use the parameter.
|
||||||
|
* required: if set to true, users need to provide this parameter unless a default is set.
|
||||||
|
* options: the different acceptable values of the parameter (i.e. an enum). If set, the value provided must match one of the options.
|
||||||
|
* default: the default value of the parameter, if none is provided in the request.
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
#!/bin/env python
|
"extra_params":{
|
||||||
#helloworld.py
|
"language": {
|
||||||
|
"aliases": ["language", "lang", "l"],
|
||||||
from senpy.plugins import AnalysisPlugin
|
"required": True,
|
||||||
from senpy.models import Sentiment
|
"options": ["es", "en"],
|
||||||
|
"default": "es"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class HelloWorld(AnalysisPlugin):
|
|
||||||
|
|
||||||
def analyse_entry(entry, params):
|
|
||||||
'''Basically do nothing with each entry'''
|
|
||||||
|
|
||||||
sentiment = Sentiment()
|
|
||||||
if len(entry.text) < self.threshold:
|
|
||||||
sentiment['marl:hasPolarity'] = 'marl:Positive'
|
|
||||||
else:
|
|
||||||
sentiment['marl:hasPolarity'] = 'marl:Negative'
|
|
||||||
entry.sentiments.append(sentiment)
|
|
||||||
yield entry
|
|
||||||
|
|
||||||
The complete code of the example plugin is available `here <https://lab.cluster.gsi.dit.upm.es/senpy/plugin-prueba>`__.
|
|
||||||
|
|
||||||
Loading data and files
|
Loading data and files
|
||||||
======================
|
======================
|
||||||
|
|
||||||
Most plugins will need access to files (dictionaries, lexicons, etc.).
|
Most plugins will need access to files (dictionaries, lexicons, etc.).
|
||||||
It is good practice to specify the paths of these files in the plugin configuration, so the same code can be reused with different resources.
|
These files are usually heavy or under a license that does not allow redistribution.
|
||||||
|
For this reason, senpy has a `data_folder` that is separated from the source files.
|
||||||
|
The location of this folder is controlled programmatically or by setting the `SENPY_DATA` environment variable.
|
||||||
|
|
||||||
|
Plugins have a convenience function `self.open` which will automatically prepend the data folder to relative paths:
|
||||||
|
|
||||||
.. code:: yaml
|
|
||||||
|
|
||||||
name: dictworld
|
|
||||||
module: dictworld
|
|
||||||
dictionary_path: <PATH OF THE FILE>
|
|
||||||
|
|
||||||
The path can be either absolute, or relative.
|
|
||||||
|
|
||||||
From absolute paths
|
|
||||||
???????????????????
|
|
||||||
|
|
||||||
Absolute paths (such as ``/data/dictionary.csv`` are straightfoward:
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
with open(os.path.join(self.dictionary_path) as f:
|
|
||||||
...
|
|
||||||
|
|
||||||
From relative paths
|
|
||||||
???????????????????
|
|
||||||
Since plugins are loading dynamically, relative paths will refer to the current working directory.
|
|
||||||
Instead, what you usually want is to load files *relative to the plugin source folder*, like so:
|
|
||||||
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
.
|
|
||||||
..
|
|
||||||
plugin.senpy
|
|
||||||
plugin.py
|
|
||||||
dictionary.csv
|
|
||||||
|
|
||||||
For this, we need to first get the path of your source folder first, like so:
|
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
import os
|
import os
|
||||||
root = os.path.realpath(__file__)
|
|
||||||
with open(os.path.join(root, self.dictionary_path) as f:
|
|
||||||
...
|
class PluginWithResources(AnalysisPlugin):
|
||||||
|
file_in_data = <FILE PATH>
|
||||||
|
file_in_sources = <FILE PATH>
|
||||||
|
|
||||||
|
def activate(self):
|
||||||
|
with self.open(self.file_in_data) as f:
|
||||||
|
self._classifier = train_from_file(f)
|
||||||
|
file_in_source = os.path.join(self.get_folder(), self.file_in_sources)
|
||||||
|
with self.open(file_in_source) as f:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
It is good practice to specify the paths of these files in the plugin configuration, so the same code can be reused with different resources.
|
||||||
|
|
||||||
|
|
||||||
Docker image
|
Docker image
|
||||||
@@ -199,7 +162,16 @@ Add the following dockerfile to your project to generate a docker image with you
|
|||||||
|
|
||||||
.. code:: dockerfile
|
.. code:: dockerfile
|
||||||
|
|
||||||
FROM gsiupm/senpy:0.8.8
|
FROM gsiupm/senpy
|
||||||
|
|
||||||
|
Once you make sure your plugin works with a specific version of senpy, modify that file to make sure your build will work even if senpy gets updated.
|
||||||
|
e.g.:
|
||||||
|
|
||||||
|
|
||||||
|
.. code:: dockerfile
|
||||||
|
|
||||||
|
FROM gsiupm/senpy:1.0.1
|
||||||
|
|
||||||
|
|
||||||
This will copy your source folder to the image, and install all dependencies.
|
This will copy your source folder to the image, and install all dependencies.
|
||||||
Now, to build an image:
|
Now, to build an image:
|
||||||
@@ -215,7 +187,7 @@ And you can run it with:
|
|||||||
docker run -p 5000:5000 gsiupm/exampleplugin
|
docker run -p 5000:5000 gsiupm/exampleplugin
|
||||||
|
|
||||||
|
|
||||||
If the plugin non-source files (:ref:`loading data and files`), the recommended way is to use absolute paths.
|
If the plugin uses non-source files (:ref:`loading data and files`), the recommended way is to use `SENPY_DATA` folder.
|
||||||
Data can then be mounted in the container or added to the image.
|
Data can then be mounted in the container or added to the image.
|
||||||
The former is recommended for open source plugins with licensed resources, whereas the latter is the most convenient and can be used for private images.
|
The former is recommended for open source plugins with licensed resources, whereas the latter is the most convenient and can be used for private images.
|
||||||
|
|
||||||
@@ -229,7 +201,7 @@ Adding data to the image:
|
|||||||
|
|
||||||
.. code:: dockerfile
|
.. code:: dockerfile
|
||||||
|
|
||||||
FROM gsiupm/senpy:0.8.8
|
FROM gsiupm/senpy:1.0.1
|
||||||
COPY data /
|
COPY data /
|
||||||
|
|
||||||
F.A.Q.
|
F.A.Q.
|
||||||
@@ -245,7 +217,7 @@ Why does the analyse function yield instead of return?
|
|||||||
??????????????????????????????????????????????????????
|
??????????????????????????????????????????????????????
|
||||||
|
|
||||||
This is so that plugins may add new entries to the response or filter some of them.
|
This is so that plugins may add new entries to the response or filter some of them.
|
||||||
For instance, a `context detection` plugin may add a new entry for each context in the original entry.
|
For instance, a chunker may split one entry into several.
|
||||||
On the other hand, a conversion plugin may leave out those entries that do not contain relevant information.
|
On the other hand, a conversion plugin may leave out those entries that do not contain relevant information.
|
||||||
|
|
||||||
|
|
||||||
@@ -275,11 +247,13 @@ Training a classifier can be time time consuming. To avoid running the training
|
|||||||
def deactivate(self):
|
def deactivate(self):
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
You can specify a 'shelf_file' in your .senpy file. By default the ShelfMixin creates a file based on the plugin name and stores it in that plugin's folder.
|
|
||||||
|
By default the ShelfMixin creates a file based on the plugin name and stores it in that plugin's folder.
|
||||||
|
However, you can manually specify a 'shelf_file' in your .senpy file.
|
||||||
|
|
||||||
Shelves may get corrupted if the plugin exists unexpectedly.
|
Shelves may get corrupted if the plugin exists unexpectedly.
|
||||||
A corrupt shelf prevents the plugin from loading.
|
A corrupt shelf prevents the plugin from loading.
|
||||||
If you do not care about the pickle, you can force your plugin to remove the corrupted file and load anyway, set the 'force_shelf' to True in your .senpy file.
|
If you do not care about the data in the shelf, you can force your plugin to remove the corrupted file and load anyway, set the 'force_shelf' to True in your plugin and start it again.
|
||||||
|
|
||||||
How can I turn an external service into a plugin?
|
How can I turn an external service into a plugin?
|
||||||
?????????????????????????????????????????????????
|
?????????????????????????????????????????????????
|
||||||
@@ -313,50 +287,11 @@ This example ilustrate how to implement a plugin that accesses the Sentiment140
|
|||||||
prefix=p,
|
prefix=p,
|
||||||
marl__hasPolarity=polarity,
|
marl__hasPolarity=polarity,
|
||||||
marl__polarityValue=polarity_value)
|
marl__polarityValue=polarity_value)
|
||||||
sentiment.prov__wasGeneratedBy = self.id
|
sentiment.prov(self)
|
||||||
entry.sentiments.append(sentiment)
|
entry.sentiments.append(sentiment)
|
||||||
yield entry
|
yield entry
|
||||||
|
|
||||||
|
|
||||||
Can my plugin require additional parameters from the user?
|
|
||||||
??????????????????????????????????????????????????????????
|
|
||||||
|
|
||||||
You can add extra parameters in the definition file under the attribute ``extra_params``.
|
|
||||||
It takes a dictionary, where the keys are the name of the argument/parameter, and the value has the following fields:
|
|
||||||
|
|
||||||
* aliases: the different names which can be used in the request to use the parameter.
|
|
||||||
* required: if set to true, users need to provide this parameter unless a default is set.
|
|
||||||
* options: the different acceptable values of the parameter (i.e. an enum). If set, the value provided must match one of the options.
|
|
||||||
* default: the default value of the parameter, if none is provided in the request.
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
extra_params
|
|
||||||
language:
|
|
||||||
aliases:
|
|
||||||
- language
|
|
||||||
- lang
|
|
||||||
- l
|
|
||||||
required: true,
|
|
||||||
options:
|
|
||||||
- es
|
|
||||||
- en
|
|
||||||
default: es
|
|
||||||
|
|
||||||
This example shows how to introduce a parameter associated with language.
|
|
||||||
The extraction of this paremeter is used in the analyse method of the Plugin interface.
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
lang = params.get("language")
|
|
||||||
|
|
||||||
Where can I set up variables for using them in my plugin?
|
|
||||||
?????????????????????????????????????????????????????????
|
|
||||||
|
|
||||||
You can add these variables in the definition file with the structure of attribute-value pairs.
|
|
||||||
|
|
||||||
Every field added to the definition file is available to the plugin instance.
|
|
||||||
|
|
||||||
Can I activate a DEBUG mode for my plugin?
|
Can I activate a DEBUG mode for my plugin?
|
||||||
???????????????????????????????????????????
|
???????????????????????????????????????????
|
||||||
|
|
||||||
@@ -371,7 +306,7 @@ Additionally, with the ``--pdb`` option you will be dropped into a pdb post mort
|
|||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
senpy --pdb
|
python -m pdb yourplugin.py
|
||||||
|
|
||||||
Where can I find more code examples?
|
Where can I find more code examples?
|
||||||
????????????????????????????????????
|
????????????????????????????????????
|
||||||
|
@@ -1,8 +1,11 @@
|
|||||||
What is Senpy?
|
What is Senpy?
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
Web services can get really complex: data validation, user interaction, formatting, logging., etc.
|
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).
|
||||||
The figure below summarizes the typical features in an analysis service.
|
|
||||||
|
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.
|
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
|
.. image:: senpy-framework.png
|
||||||
|
@@ -7,7 +7,8 @@ The senpy server is launched via the `senpy` command:
|
|||||||
|
|
||||||
usage: senpy [-h] [--level logging_level] [--debug] [--default-plugins]
|
usage: senpy [-h] [--level logging_level] [--debug] [--default-plugins]
|
||||||
[--host HOST] [--port PORT] [--plugins-folder PLUGINS_FOLDER]
|
[--host HOST] [--port PORT] [--plugins-folder PLUGINS_FOLDER]
|
||||||
[--only-install]
|
[--only-install] [--only-list] [--data-folder DATA_FOLDER]
|
||||||
|
[--threaded] [--version]
|
||||||
|
|
||||||
Run a Senpy server
|
Run a Senpy server
|
||||||
|
|
||||||
@@ -22,6 +23,13 @@ The senpy server is launched via the `senpy` command:
|
|||||||
--plugins-folder PLUGINS_FOLDER, -f PLUGINS_FOLDER
|
--plugins-folder PLUGINS_FOLDER, -f PLUGINS_FOLDER
|
||||||
Where to look for plugins.
|
Where to look for plugins.
|
||||||
--only-install, -i Do not run a server, only install plugin dependencies
|
--only-install, -i Do not run a server, only install plugin dependencies
|
||||||
|
--only-list, --list Do not run a server, only list plugins found
|
||||||
|
--data-folder DATA_FOLDER, --data DATA_FOLDER
|
||||||
|
Where to look for data. It be set with the SENPY_DATA
|
||||||
|
environment variable as well.
|
||||||
|
--threaded Run a threaded server
|
||||||
|
--version, -v Output the senpy version and exit
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
When launched, the server will recursively look for plugins in the specified plugins folder (the current working directory by default).
|
When launched, the server will recursively look for plugins in the specified plugins folder (the current working directory by default).
|
||||||
|
@@ -1,8 +1,24 @@
|
|||||||
Vocabularies and model
|
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.
|
Senpy has been applied to sentiment and emotion analysis services using the following vocabularies:
|
||||||
* 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
|
* 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.
|
||||||
|
23
example-plugins/README.md
Normal file
23
example-plugins/README.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
This is a collection of plugins that exemplify certain aspects of plugin development with senpy.
|
||||||
|
|
||||||
|
The first series of plugins the `basic` ones.
|
||||||
|
Their starting point is a classification function defined in `basic.py`.
|
||||||
|
They all include testing and running them as a script will run all tests.
|
||||||
|
In ascending order of customization, the plugins are:
|
||||||
|
|
||||||
|
* Basic is the simplest plugin of all. It leverages the `SentimentBox` Plugin class to create a plugin out of a classification method, and `MappingMixin` to convert the labels from (`pos`, `neg`) to (`marl:Positive`, `marl:Negative`
|
||||||
|
* Basic_box is just like the previous one, but replaces the mixin with a custom function.
|
||||||
|
* Basic_configurable is a version of `basic` with a configurable map of emojis for each sentiment.
|
||||||
|
* Basic_parameterized like `basic_info`, but users set the map in each query (via `extra_parameters`).
|
||||||
|
* Basic_analyse\_entry uses the more general `analyse_entry` method and adds the annotations individually.
|
||||||
|
|
||||||
|
|
||||||
|
In rest of the plugins show advanced topics:
|
||||||
|
|
||||||
|
* mynoop: shows how to add a definition file with external requirements for a plugin. Doing this with a python-only module would require moving all imports of the requirements to their functions, which is considered bad practice.
|
||||||
|
* Async: a barebones example of training a plugin and analyzing data in parallel.
|
||||||
|
|
||||||
|
All of the plugins in this folder include a set of test cases and they are periodically tested with the latest version of senpy.
|
||||||
|
|
||||||
|
Additioanlly, for an example of stand-alone plugin that can be tested and deployed with docker, take a look at: lab.cluster.gsi.dit.upm.es/senpy/plugin-example
|
||||||
|
bbm
|
@@ -1,4 +1,4 @@
|
|||||||
from senpy.plugins import AnalysisPlugin
|
from senpy import AnalysisPlugin
|
||||||
|
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
|
||||||
@@ -7,10 +7,15 @@ def _train(process_number):
|
|||||||
return process_number
|
return process_number
|
||||||
|
|
||||||
|
|
||||||
class AsyncPlugin(AnalysisPlugin):
|
class Async(AnalysisPlugin):
|
||||||
|
'''An example of an asynchronous module'''
|
||||||
|
author = '@balkian'
|
||||||
|
version = '0.2'
|
||||||
|
async = True
|
||||||
|
|
||||||
def _do_async(self, num_processes):
|
def _do_async(self, num_processes):
|
||||||
pool = multiprocessing.Pool(processes=num_processes)
|
pool = multiprocessing.Pool(processes=num_processes)
|
||||||
values = pool.map(_train, range(num_processes))
|
values = sorted(pool.map(_train, range(num_processes)))
|
||||||
|
|
||||||
return values
|
return values
|
||||||
|
|
||||||
@@ -22,5 +27,11 @@ class AsyncPlugin(AnalysisPlugin):
|
|||||||
entry.async_values = values
|
entry.async_values = values
|
||||||
yield entry
|
yield entry
|
||||||
|
|
||||||
def test(self):
|
test_cases = [
|
||||||
pass
|
{
|
||||||
|
'input': 'any',
|
||||||
|
'expected': {
|
||||||
|
'async_values': [0, 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
23
example-plugins/basic.py
Normal file
23
example-plugins/basic.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/local/bin/python
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
emoticons = {
|
||||||
|
'pos': [':)', ':]', '=)', ':D'],
|
||||||
|
'neg': [':(', ':[', '=(']
|
||||||
|
}
|
||||||
|
|
||||||
|
emojis = {
|
||||||
|
'pos': ['😁', '😂', '😃', '😄', '😆', '😅', '😄' '😍'],
|
||||||
|
'neg': ['😢', '😡', '😠', '😞', '😖', '😔', '😓', '😒']
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_polarity(text, dictionaries=[emoticons, emojis]):
|
||||||
|
polarity = 'marl:Neutral'
|
||||||
|
for dictionary in dictionaries:
|
||||||
|
for label, values in dictionary.items():
|
||||||
|
for emoticon in values:
|
||||||
|
if emoticon and emoticon in text:
|
||||||
|
polarity = label
|
||||||
|
break
|
||||||
|
return polarity
|
47
example-plugins/basic_analyse_entry_plugin.py
Normal file
47
example-plugins/basic_analyse_entry_plugin.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/local/bin/python
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
from senpy import easy_test, models, plugins
|
||||||
|
|
||||||
|
import basic
|
||||||
|
|
||||||
|
|
||||||
|
class BasicAnalyseEntry(plugins.SentimentPlugin):
|
||||||
|
'''Equivalent to Basic, implementing the analyse_entry method'''
|
||||||
|
|
||||||
|
author = '@balkian'
|
||||||
|
version = '0.1'
|
||||||
|
|
||||||
|
mappings = {
|
||||||
|
'pos': 'marl:Positive',
|
||||||
|
'neg': 'marl:Negative',
|
||||||
|
'default': 'marl:Neutral'
|
||||||
|
}
|
||||||
|
|
||||||
|
def analyse_entry(self, entry, params):
|
||||||
|
polarity = basic.get_polarity(entry.text)
|
||||||
|
|
||||||
|
polarity = self.mappings.get(polarity, self.mappings['default'])
|
||||||
|
|
||||||
|
s = models.Sentiment(marl__hasPolarity=polarity)
|
||||||
|
s.prov(self)
|
||||||
|
entry.sentiments.append(s)
|
||||||
|
yield entry
|
||||||
|
|
||||||
|
test_cases = [{
|
||||||
|
'input': 'Hello :)',
|
||||||
|
'polarity': 'marl:Positive'
|
||||||
|
}, {
|
||||||
|
'input': 'So sad :(',
|
||||||
|
'polarity': 'marl:Negative'
|
||||||
|
}, {
|
||||||
|
'input': 'Yay! Emojis 😁',
|
||||||
|
'polarity': 'marl:Positive'
|
||||||
|
}, {
|
||||||
|
'input': 'But no emoticons 😢',
|
||||||
|
'polarity': 'marl:Negative'
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
easy_test()
|
41
example-plugins/basic_box_plugin.py
Normal file
41
example-plugins/basic_box_plugin.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/local/bin/python
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
from senpy import easy_test, SentimentBox
|
||||||
|
|
||||||
|
import basic
|
||||||
|
|
||||||
|
|
||||||
|
class BasicBox(SentimentBox):
|
||||||
|
''' A modified version of Basic that also does converts annotations manually'''
|
||||||
|
|
||||||
|
author = '@balkian'
|
||||||
|
version = '0.1'
|
||||||
|
|
||||||
|
mappings = {
|
||||||
|
'pos': 'marl:Positive',
|
||||||
|
'neg': 'marl:Negative',
|
||||||
|
'default': 'marl:Neutral'
|
||||||
|
}
|
||||||
|
|
||||||
|
def predict_one(self, input):
|
||||||
|
output = basic.get_polarity(input)
|
||||||
|
return self.mappings.get(output, self.mappings['default'])
|
||||||
|
|
||||||
|
test_cases = [{
|
||||||
|
'input': 'Hello :)',
|
||||||
|
'polarity': 'marl:Positive'
|
||||||
|
}, {
|
||||||
|
'input': 'So sad :(',
|
||||||
|
'polarity': 'marl:Negative'
|
||||||
|
}, {
|
||||||
|
'input': 'Yay! Emojis 😁',
|
||||||
|
'polarity': 'marl:Positive'
|
||||||
|
}, {
|
||||||
|
'input': 'But no emoticons 😢',
|
||||||
|
'polarity': 'marl:Negative'
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
easy_test()
|
40
example-plugins/basic_plugin.py
Normal file
40
example-plugins/basic_plugin.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/local/bin/python
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
from senpy import easy_test, SentimentBox, MappingMixin
|
||||||
|
|
||||||
|
import basic
|
||||||
|
|
||||||
|
|
||||||
|
class Basic(MappingMixin, SentimentBox):
|
||||||
|
'''Provides sentiment annotation using a lexicon'''
|
||||||
|
|
||||||
|
author = '@balkian'
|
||||||
|
version = '0.1'
|
||||||
|
|
||||||
|
mappings = {
|
||||||
|
'pos': 'marl:Positive',
|
||||||
|
'neg': 'marl:Negative',
|
||||||
|
'default': 'marl:Neutral'
|
||||||
|
}
|
||||||
|
|
||||||
|
def predict_one(self, input):
|
||||||
|
return basic.get_polarity(input)
|
||||||
|
|
||||||
|
test_cases = [{
|
||||||
|
'input': 'Hello :)',
|
||||||
|
'polarity': 'marl:Positive'
|
||||||
|
}, {
|
||||||
|
'input': 'So sad :(',
|
||||||
|
'polarity': 'marl:Negative'
|
||||||
|
}, {
|
||||||
|
'input': 'Yay! Emojis 😁',
|
||||||
|
'polarity': 'marl:Positive'
|
||||||
|
}, {
|
||||||
|
'input': 'But no emoticons 😢',
|
||||||
|
'polarity': 'marl:Negative'
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
easy_test()
|
106
example-plugins/configurable_plugin.py
Normal file
106
example-plugins/configurable_plugin.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/local/bin/python
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
from senpy import easy_test, models, plugins
|
||||||
|
|
||||||
|
import basic
|
||||||
|
|
||||||
|
|
||||||
|
class Dictionary(plugins.SentimentPlugin):
|
||||||
|
'''Sentiment annotation using a configurable lexicon'''
|
||||||
|
|
||||||
|
author = '@balkian'
|
||||||
|
version = '0.2'
|
||||||
|
|
||||||
|
dictionaries = [basic.emojis, basic.emoticons]
|
||||||
|
|
||||||
|
mappings = {'pos': 'marl:Positive', 'neg': 'marl:Negative'}
|
||||||
|
|
||||||
|
def analyse_entry(self, entry, params):
|
||||||
|
polarity = basic.get_polarity(entry.text, self.dictionaries)
|
||||||
|
if polarity in self.mappings:
|
||||||
|
polarity = self.mappings[polarity]
|
||||||
|
|
||||||
|
s = models.Sentiment(marl__hasPolarity=polarity)
|
||||||
|
s.prov(self)
|
||||||
|
entry.sentiments.append(s)
|
||||||
|
yield entry
|
||||||
|
|
||||||
|
test_cases = [{
|
||||||
|
'input': 'Hello :)',
|
||||||
|
'polarity': 'marl:Positive'
|
||||||
|
}, {
|
||||||
|
'input': 'So sad :(',
|
||||||
|
'polarity': 'marl:Negative'
|
||||||
|
}, {
|
||||||
|
'input': 'Yay! Emojis 😁',
|
||||||
|
'polarity': 'marl:Positive'
|
||||||
|
}, {
|
||||||
|
'input': 'But no emoticons 😢',
|
||||||
|
'polarity': 'marl:Negative'
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
class EmojiOnly(Dictionary):
|
||||||
|
'''Sentiment annotation with a basic lexicon of emojis'''
|
||||||
|
description = 'A plugin'
|
||||||
|
dictionaries = [basic.emojis]
|
||||||
|
|
||||||
|
test_cases = [{
|
||||||
|
'input': 'Hello :)',
|
||||||
|
'polarity': 'marl:Neutral'
|
||||||
|
}, {
|
||||||
|
'input': 'So sad :(',
|
||||||
|
'polarity': 'marl:Neutral'
|
||||||
|
}, {
|
||||||
|
'input': 'Yay! Emojis 😁',
|
||||||
|
'polarity': 'marl:Positive'
|
||||||
|
}, {
|
||||||
|
'input': 'But no emoticons 😢',
|
||||||
|
'polarity': 'marl:Negative'
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
class EmoticonsOnly(Dictionary):
|
||||||
|
'''Sentiment annotation with a basic lexicon of emoticons'''
|
||||||
|
dictionaries = [basic.emoticons]
|
||||||
|
|
||||||
|
test_cases = [{
|
||||||
|
'input': 'Hello :)',
|
||||||
|
'polarity': 'marl:Positive'
|
||||||
|
}, {
|
||||||
|
'input': 'So sad :(',
|
||||||
|
'polarity': 'marl:Negative'
|
||||||
|
}, {
|
||||||
|
'input': 'Yay! Emojis 😁',
|
||||||
|
'polarity': 'marl:Neutral'
|
||||||
|
}, {
|
||||||
|
'input': 'But no emoticons 😢',
|
||||||
|
'polarity': 'marl:Neutral'
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
class Salutes(Dictionary):
|
||||||
|
'''Sentiment annotation with a custom lexicon, for illustration purposes'''
|
||||||
|
dictionaries = [{
|
||||||
|
'marl:Positive': ['Hello', '!'],
|
||||||
|
'marl:Negative': ['Good bye', ]
|
||||||
|
}]
|
||||||
|
|
||||||
|
test_cases = [{
|
||||||
|
'input': 'Hello :)',
|
||||||
|
'polarity': 'marl:Positive'
|
||||||
|
}, {
|
||||||
|
'input': 'Good bye :(',
|
||||||
|
'polarity': 'marl:Negative'
|
||||||
|
}, {
|
||||||
|
'input': 'Yay! Emojis 😁',
|
||||||
|
'polarity': 'marl:Positive'
|
||||||
|
}, {
|
||||||
|
'input': 'But no emoticons 😢',
|
||||||
|
'polarity': 'marl:Neutral'
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
easy_test()
|
25
example-plugins/dummy_plugin.py
Normal file
25
example-plugins/dummy_plugin.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from senpy import AnalysisPlugin, easy
|
||||||
|
|
||||||
|
|
||||||
|
class Dummy(AnalysisPlugin):
|
||||||
|
'''This is a dummy self-contained plugin'''
|
||||||
|
author = '@balkian'
|
||||||
|
version = '0.1'
|
||||||
|
|
||||||
|
def analyse_entry(self, entry, params):
|
||||||
|
entry['nif:isString'] = entry['nif:isString'][::-1]
|
||||||
|
entry.reversed = entry.get('reversed', 0) + 1
|
||||||
|
yield entry
|
||||||
|
|
||||||
|
test_cases = [{
|
||||||
|
'entry': {
|
||||||
|
'nif:isString': 'Hello',
|
||||||
|
},
|
||||||
|
'expected': {
|
||||||
|
'nif:isString': 'olleH'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
easy()
|
40
example-plugins/dummy_required_plugin.py
Normal file
40
example-plugins/dummy_required_plugin.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from senpy import AnalysisPlugin, easy
|
||||||
|
|
||||||
|
|
||||||
|
class DummyRequired(AnalysisPlugin):
|
||||||
|
'''This is a dummy self-contained plugin'''
|
||||||
|
author = '@balkian'
|
||||||
|
version = '0.1'
|
||||||
|
extra_params = {
|
||||||
|
'example': {
|
||||||
|
'description': 'An example parameter',
|
||||||
|
'required': True,
|
||||||
|
'options': ['a', 'b']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def analyse_entry(self, entry, params):
|
||||||
|
entry['nif:isString'] = entry['nif:isString'][::-1]
|
||||||
|
entry.reversed = entry.get('reversed', 0) + 1
|
||||||
|
yield entry
|
||||||
|
|
||||||
|
test_cases = [{
|
||||||
|
'entry': {
|
||||||
|
'nif:isString': 'Hello',
|
||||||
|
},
|
||||||
|
'should_fail': True
|
||||||
|
}, {
|
||||||
|
'entry': {
|
||||||
|
'nif:isString': 'Hello',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'example': 'a'
|
||||||
|
},
|
||||||
|
'expected': {
|
||||||
|
'nif:isString': 'olleH'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
easy()
|
24
example-plugins/mynoop.py
Normal file
24
example-plugins/mynoop.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import noop
|
||||||
|
from senpy.plugins import SentimentPlugin
|
||||||
|
|
||||||
|
|
||||||
|
class NoOp(SentimentPlugin):
|
||||||
|
'''This plugin does nothing. Literally nothing.'''
|
||||||
|
|
||||||
|
version = 0
|
||||||
|
|
||||||
|
def analyse_entry(self, entry, *args, **kwargs):
|
||||||
|
yield entry
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
print(dir(noop))
|
||||||
|
super(NoOp, self).test()
|
||||||
|
|
||||||
|
test_cases = [{
|
||||||
|
'entry': {
|
||||||
|
'nif:isString': 'hello'
|
||||||
|
},
|
||||||
|
'expected': {
|
||||||
|
'nif:isString': 'hello'
|
||||||
|
}
|
||||||
|
}]
|
3
example-plugins/mynoop.senpy
Normal file
3
example-plugins/mynoop.senpy
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module: mynoop
|
||||||
|
requirements:
|
||||||
|
- noop
|
63
example-plugins/parameterized_plugin.py
Normal file
63
example-plugins/parameterized_plugin.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/local/bin/python
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
from senpy import easy_test, models, plugins
|
||||||
|
|
||||||
|
import basic
|
||||||
|
|
||||||
|
|
||||||
|
class ParameterizedDictionary(plugins.SentimentPlugin):
|
||||||
|
'''This is a basic self-contained plugin'''
|
||||||
|
|
||||||
|
author = '@balkian'
|
||||||
|
version = '0.2'
|
||||||
|
|
||||||
|
extra_params = {
|
||||||
|
'positive-words': {
|
||||||
|
'description': 'Comma-separated list of words that are considered positive',
|
||||||
|
'aliases': ['positive'],
|
||||||
|
'required': True
|
||||||
|
},
|
||||||
|
'negative-words': {
|
||||||
|
'description': 'Comma-separated list of words that are considered negative',
|
||||||
|
'aliases': ['negative'],
|
||||||
|
'required': False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def analyse_entry(self, entry, params):
|
||||||
|
positive_words = params['positive-words'].split(',')
|
||||||
|
negative_words = params['negative-words'].split(',')
|
||||||
|
dictionary = {
|
||||||
|
'marl:Positive': positive_words,
|
||||||
|
'marl:Negative': negative_words,
|
||||||
|
}
|
||||||
|
polarity = basic.get_polarity(entry.text, [dictionary])
|
||||||
|
|
||||||
|
s = models.Sentiment(marl__hasPolarity=polarity)
|
||||||
|
s.prov(self)
|
||||||
|
entry.sentiments.append(s)
|
||||||
|
yield entry
|
||||||
|
|
||||||
|
test_cases = [
|
||||||
|
{
|
||||||
|
'input': 'Hello :)',
|
||||||
|
'polarity': 'marl:Positive',
|
||||||
|
'parameters': {
|
||||||
|
'positive': "Hello,:)",
|
||||||
|
'negative': "sad,:()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'input': 'Hello :)',
|
||||||
|
'polarity': 'marl:Negative',
|
||||||
|
'parameters': {
|
||||||
|
'positive': "",
|
||||||
|
'negative': "Hello"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
easy_test()
|
33
example-plugins/sklearn/mydata.py
Normal file
33
example-plugins/sklearn/mydata.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
'''
|
||||||
|
Create a dummy dataset.
|
||||||
|
Messages with a happy emoticon are labelled positive
|
||||||
|
Messages with a sad emoticon are labelled negative
|
||||||
|
'''
|
||||||
|
import random
|
||||||
|
|
||||||
|
dataset = []
|
||||||
|
|
||||||
|
vocabulary = ['hello', 'world', 'senpy', 'cool', 'goodbye', 'random', 'text']
|
||||||
|
|
||||||
|
emojimap = {
|
||||||
|
1: [':)', ],
|
||||||
|
-1: [':(', ]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for tag, values in emojimap.items():
|
||||||
|
for i in range(1000):
|
||||||
|
msg = ''
|
||||||
|
for j in range(3):
|
||||||
|
msg += random.choice(vocabulary)
|
||||||
|
msg += " "
|
||||||
|
msg += random.choice(values)
|
||||||
|
dataset.append([msg, tag])
|
||||||
|
|
||||||
|
|
||||||
|
text = []
|
||||||
|
labels = []
|
||||||
|
|
||||||
|
for i in dataset:
|
||||||
|
text.append(i[0])
|
||||||
|
labels.append(i[1])
|
30
example-plugins/sklearn/mypipeline.py
Normal file
30
example-plugins/sklearn/mypipeline.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from sklearn.pipeline import Pipeline
|
||||||
|
from sklearn.feature_extraction.text import CountVectorizer
|
||||||
|
from sklearn.model_selection import train_test_split
|
||||||
|
|
||||||
|
from mydata import text, labels
|
||||||
|
|
||||||
|
X_train, X_test, y_train, y_test = train_test_split(text, labels, test_size=0.12, random_state=42)
|
||||||
|
|
||||||
|
from sklearn.naive_bayes import MultinomialNB
|
||||||
|
|
||||||
|
|
||||||
|
count_vec = CountVectorizer(tokenizer=lambda x: x.split())
|
||||||
|
clf3 = MultinomialNB()
|
||||||
|
pipeline = Pipeline([('cv', count_vec),
|
||||||
|
('clf', clf3)])
|
||||||
|
|
||||||
|
pipeline.fit(X_train, y_train)
|
||||||
|
print('Feature names: {}'.format(count_vec.get_feature_names()))
|
||||||
|
print('Class count: {}'.format(clf3.class_count_))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print('--Results--')
|
||||||
|
tests = [
|
||||||
|
(['The sentiment for senpy should be positive :)', ], 1),
|
||||||
|
(['The sentiment for anything else should be negative :()', ], -1)
|
||||||
|
]
|
||||||
|
for features, expected in tests:
|
||||||
|
result = pipeline.predict(features)
|
||||||
|
print('Input: {}\nExpected: {}\nGot: {}'.format(features[0], expected, result))
|
37
example-plugins/sklearn/pipeline_plugin.py
Normal file
37
example-plugins/sklearn/pipeline_plugin.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from senpy import SentimentBox, MappingMixin, easy_test
|
||||||
|
|
||||||
|
from mypipeline import pipeline
|
||||||
|
|
||||||
|
|
||||||
|
class PipelineSentiment(MappingMixin, SentimentBox):
|
||||||
|
'''
|
||||||
|
This is a pipeline plugin that wraps a classifier defined in another module
|
||||||
|
(mypipeline).
|
||||||
|
'''
|
||||||
|
author = '@balkian'
|
||||||
|
version = 0.1
|
||||||
|
maxPolarityValue = 1
|
||||||
|
minPolarityValue = -1
|
||||||
|
|
||||||
|
mappings = {
|
||||||
|
1: 'marl:Positive',
|
||||||
|
-1: 'marl:Negative'
|
||||||
|
}
|
||||||
|
|
||||||
|
def predict_one(self, input):
|
||||||
|
return pipeline.predict([input, ])[0]
|
||||||
|
|
||||||
|
test_cases = [
|
||||||
|
{
|
||||||
|
'input': 'The sentiment for senpy should be positive :)',
|
||||||
|
'polarity': 'marl:Positive'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'input': 'The sentiment for senpy should be negative :(',
|
||||||
|
'polarity': 'marl:Negative'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
easy_test()
|
27
example-plugins/sleep_plugin.py
Normal file
27
example-plugins/sleep_plugin.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from senpy.plugins import AnalysisPlugin
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
|
||||||
|
class Sleep(AnalysisPlugin):
|
||||||
|
'''Dummy plugin to test async'''
|
||||||
|
author = "@balkian"
|
||||||
|
version = "0.2"
|
||||||
|
timeout = 0.05
|
||||||
|
extra_params = {
|
||||||
|
"timeout": {
|
||||||
|
"@id": "timeout_sleep",
|
||||||
|
"aliases": ["timeout", "to"],
|
||||||
|
"required": False,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def activate(self, *args, **kwargs):
|
||||||
|
sleep(self.timeout)
|
||||||
|
|
||||||
|
def analyse_entry(self, entry, params):
|
||||||
|
sleep(float(params.get("timeout", self.timeout)))
|
||||||
|
yield entry
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
pass
|
1
extra-requirements.txt
Normal file
1
extra-requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
gsitk
|
@@ -13,7 +13,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: senpy-latest
|
- name: senpy-latest
|
||||||
image: gsiupm/senpy:latest
|
image: $IMAGEWTAG
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
args:
|
args:
|
||||||
- "--default-plugins"
|
- "--default-plugins"
|
||||||
|
@@ -3,7 +3,6 @@ requests>=2.4.1
|
|||||||
tornado>=4.4.3
|
tornado>=4.4.3
|
||||||
PyLD>=0.6.5
|
PyLD>=0.6.5
|
||||||
nltk
|
nltk
|
||||||
six
|
|
||||||
future
|
future
|
||||||
jsonschema
|
jsonschema
|
||||||
jsonref
|
jsonref
|
||||||
|
@@ -25,4 +25,10 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
logger.info('Using senpy version: {}'.format(__version__))
|
logger.info('Using senpy version: {}'.format(__version__))
|
||||||
|
|
||||||
|
from .utils import easy, easy_load, easy_test # noqa: F401
|
||||||
|
|
||||||
|
from .models import * # noqa: F401,F403
|
||||||
|
from .plugins import * # noqa: F401,F403
|
||||||
|
from .extensions import * # noqa: F401,F403
|
||||||
|
|
||||||
__all__ = ['api', 'blueprints', 'cli', 'extensions', 'models', 'plugins']
|
__all__ = ['api', 'blueprints', 'cli', 'extensions', 'models', 'plugins']
|
||||||
|
@@ -22,9 +22,11 @@ the server.
|
|||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from senpy.extensions import Senpy
|
from senpy.extensions import Senpy
|
||||||
|
from senpy.utils import easy_test
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
import senpy
|
import senpy
|
||||||
|
|
||||||
@@ -38,7 +40,7 @@ def main():
|
|||||||
'-l',
|
'-l',
|
||||||
metavar='logging_level',
|
metavar='logging_level',
|
||||||
type=str,
|
type=str,
|
||||||
default="INFO",
|
default="WARN",
|
||||||
help='Logging level')
|
help='Logging level')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--debug',
|
'--debug',
|
||||||
@@ -66,7 +68,7 @@ def main():
|
|||||||
'--plugins-folder',
|
'--plugins-folder',
|
||||||
'-f',
|
'-f',
|
||||||
type=str,
|
type=str,
|
||||||
default='plugins',
|
default='.',
|
||||||
help='Where to look for plugins.')
|
help='Where to look for plugins.')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--only-install',
|
'--only-install',
|
||||||
@@ -74,6 +76,24 @@ def main():
|
|||||||
action='store_true',
|
action='store_true',
|
||||||
default=False,
|
default=False,
|
||||||
help='Do not run a server, only install plugin dependencies')
|
help='Do not run a server, only install plugin dependencies')
|
||||||
|
parser.add_argument(
|
||||||
|
'--only-test',
|
||||||
|
'-t',
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
help='Do not run a server, just test all plugins')
|
||||||
|
parser.add_argument(
|
||||||
|
'--only-list',
|
||||||
|
'--list',
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
help='Do not run a server, only list plugins found')
|
||||||
|
parser.add_argument(
|
||||||
|
'--data-folder',
|
||||||
|
'--data',
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help='Where to look for data. It be set with the SENPY_DATA environment variable as well.')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--threaded',
|
'--threaded',
|
||||||
action='store_false',
|
action='store_false',
|
||||||
@@ -88,17 +108,30 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if args.version:
|
if args.version:
|
||||||
print('Senpy version {}'.format(senpy.__version__))
|
print('Senpy version {}'.format(senpy.__version__))
|
||||||
|
print(sys.version)
|
||||||
exit(1)
|
exit(1)
|
||||||
logging.basicConfig()
|
|
||||||
rl = logging.getLogger()
|
rl = logging.getLogger()
|
||||||
rl.setLevel(getattr(logging, args.level))
|
rl.setLevel(getattr(logging, args.level))
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.debug = args.debug
|
app.debug = args.debug
|
||||||
sp = Senpy(app, args.plugins_folder, default_plugins=args.default_plugins)
|
sp = Senpy(app, args.plugins_folder,
|
||||||
|
default_plugins=args.default_plugins,
|
||||||
|
data_folder=args.data_folder)
|
||||||
|
if args.only_list:
|
||||||
|
plugins = sp.plugins()
|
||||||
|
maxwidth = max(len(x.id) for x in plugins)
|
||||||
|
for plugin in plugins:
|
||||||
|
import inspect
|
||||||
|
fpath = inspect.getfile(plugin.__class__)
|
||||||
|
print('{: <{width}} @ {}'.format(plugin.id, fpath, width=maxwidth))
|
||||||
|
return
|
||||||
sp.install_deps()
|
sp.install_deps()
|
||||||
if args.only_install:
|
if args.only_install:
|
||||||
return
|
return
|
||||||
sp.activate_all()
|
sp.activate_all()
|
||||||
|
if args.only_test:
|
||||||
|
easy_test(sp.plugins())
|
||||||
|
return
|
||||||
print('Senpy version {}'.format(senpy.__version__))
|
print('Senpy version {}'.format(senpy.__version__))
|
||||||
print('Server running on port %s:%d. Ctrl+C to quit' % (args.host,
|
print('Server running on port %s:%d. Ctrl+C to quit' % (args.host,
|
||||||
args.port))
|
args.port))
|
||||||
|
64
senpy/api.py
64
senpy/api.py
@@ -13,8 +13,9 @@ API_PARAMS = {
|
|||||||
"expanded-jsonld": {
|
"expanded-jsonld": {
|
||||||
"@id": "expanded-jsonld",
|
"@id": "expanded-jsonld",
|
||||||
"aliases": ["expanded"],
|
"aliases": ["expanded"],
|
||||||
|
"options": "boolean",
|
||||||
"required": True,
|
"required": True,
|
||||||
"default": 0
|
"default": False
|
||||||
},
|
},
|
||||||
"with_parameters": {
|
"with_parameters": {
|
||||||
"aliases": ['withparameters',
|
"aliases": ['withparameters',
|
||||||
@@ -23,13 +24,6 @@ API_PARAMS = {
|
|||||||
"default": False,
|
"default": False,
|
||||||
"required": True
|
"required": True
|
||||||
},
|
},
|
||||||
"plugin_type": {
|
|
||||||
"@id": "pluginType",
|
|
||||||
"description": 'What kind of plugins to list',
|
|
||||||
"aliases": ["pluginType"],
|
|
||||||
"required": True,
|
|
||||||
"default": "analysisPlugin"
|
|
||||||
},
|
|
||||||
"outformat": {
|
"outformat": {
|
||||||
"@id": "outformat",
|
"@id": "outformat",
|
||||||
"aliases": ["o"],
|
"aliases": ["o"],
|
||||||
@@ -59,6 +53,31 @@ 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",
|
||||||
|
"description": 'What kind of plugins to list',
|
||||||
|
"aliases": ["pluginType"],
|
||||||
|
"required": True,
|
||||||
|
"default": 'analysisPlugin'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
WEB_PARAMS = {
|
WEB_PARAMS = {
|
||||||
"inHeaders": {
|
"inHeaders": {
|
||||||
"aliases": ["headers"],
|
"aliases": ["headers"],
|
||||||
@@ -95,7 +114,7 @@ NIF_PARAMS = {
|
|||||||
"aliases": ["f"],
|
"aliases": ["f"],
|
||||||
"required": False,
|
"required": False,
|
||||||
"default": "text",
|
"default": "text",
|
||||||
"options": ["turtle", "text", "json-ld"],
|
"options": ["text", "json-ld"],
|
||||||
},
|
},
|
||||||
"language": {
|
"language": {
|
||||||
"@id": "language",
|
"@id": "language",
|
||||||
@@ -126,39 +145,38 @@ def parse_params(indict, *specs):
|
|||||||
wrong_params = {}
|
wrong_params = {}
|
||||||
for spec in specs:
|
for spec in specs:
|
||||||
for param, options in iteritems(spec):
|
for param, options in iteritems(spec):
|
||||||
if param[0] != "@": # Exclude json-ld properties
|
|
||||||
for alias in options.get("aliases", []):
|
for alias in options.get("aliases", []):
|
||||||
# Replace each alias with the correct name of the parameter
|
# Replace each alias with the correct name of the parameter
|
||||||
if alias in indict and alias is not param:
|
if alias in indict and alias is not param:
|
||||||
outdict[param] = indict[alias]
|
outdict[param] = indict[alias]
|
||||||
del indict[alias]
|
del outdict[alias]
|
||||||
continue
|
continue
|
||||||
if param not in outdict:
|
if param not in outdict:
|
||||||
if options.get("required", False) and "default" not in options:
|
|
||||||
wrong_params[param] = spec[param]
|
|
||||||
else:
|
|
||||||
if "default" in options:
|
if "default" in options:
|
||||||
|
# We assume the default is correct
|
||||||
outdict[param] = options["default"]
|
outdict[param] = options["default"]
|
||||||
elif "options" in spec[param]:
|
elif options.get("required", False):
|
||||||
if spec[param]["options"] == "boolean":
|
wrong_params[param] = spec[param]
|
||||||
|
continue
|
||||||
|
if "options" in options:
|
||||||
|
if options["options"] == "boolean":
|
||||||
outdict[param] = outdict[param] in [None, True, 'true', '1']
|
outdict[param] = outdict[param] in [None, True, 'true', '1']
|
||||||
elif outdict[param] not in spec[param]["options"]:
|
elif outdict[param] not in options["options"]:
|
||||||
wrong_params[param] = spec[param]
|
wrong_params[param] = spec[param]
|
||||||
if wrong_params:
|
if wrong_params:
|
||||||
logger.debug("Error parsing: %s", wrong_params)
|
logger.debug("Error parsing: %s", wrong_params)
|
||||||
message = Error(
|
message = Error(
|
||||||
status=400,
|
status=400,
|
||||||
message="Missing or invalid parameters",
|
message='Missing or invalid parameters',
|
||||||
parameters=outdict,
|
parameters=outdict,
|
||||||
errors={param: error
|
errors=wrong_params)
|
||||||
for param, error in iteritems(wrong_params)})
|
|
||||||
raise message
|
raise message
|
||||||
if 'algorithm' in outdict and not isinstance(outdict['algorithm'], list):
|
if 'algorithm' in outdict and not isinstance(outdict['algorithm'], list):
|
||||||
outdict['algorithm'] = outdict['algorithm'].split(',')
|
outdict['algorithm'] = outdict['algorithm'].split(',')
|
||||||
return outdict
|
return outdict
|
||||||
|
|
||||||
|
|
||||||
def get_extra_params(request, plugin=None):
|
def parse_extra_params(request, plugin=None):
|
||||||
params = request.parameters.copy()
|
params = request.parameters.copy()
|
||||||
if plugin:
|
if plugin:
|
||||||
extra_params = parse_params(params, plugin.get('extra_params', {}))
|
extra_params = parse_params(params, plugin.get('extra_params', {}))
|
||||||
@@ -176,7 +194,7 @@ def parse_call(params):
|
|||||||
results.entries.append(entry)
|
results.entries.append(entry)
|
||||||
elif params['informat'] == 'json-ld':
|
elif params['informat'] == 'json-ld':
|
||||||
results = from_string(params['input'], cls=Results)
|
results = from_string(params['input'], cls=Results)
|
||||||
else:
|
else: # pragma: no cover
|
||||||
raise NotImplemented('Informat {} is not implemented'.format(params['informat']))
|
raise NotImplementedError('Informat {} is not implemented'.format(params['informat']))
|
||||||
results.parameters = params
|
results.parameters = params
|
||||||
return results
|
return results
|
||||||
|
@@ -19,18 +19,19 @@ Blueprints for Senpy
|
|||||||
"""
|
"""
|
||||||
from flask import (Blueprint, request, current_app, render_template, url_for,
|
from flask import (Blueprint, request, current_app, render_template, url_for,
|
||||||
jsonify)
|
jsonify)
|
||||||
from .models import Error, Response, Help, Plugins, read_schema
|
from .models import Error, Response, Help, Plugins, read_schema, Datasets
|
||||||
from . import api
|
from . import api
|
||||||
from .version import __version__
|
from .version import __version__
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import traceback
|
||||||
import json
|
import json
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
api_blueprint = Blueprint("api", __name__)
|
api_blueprint = Blueprint("api", __name__)
|
||||||
demo_blueprint = Blueprint("demo", __name__)
|
demo_blueprint = Blueprint("demo", __name__, template_folder='templates')
|
||||||
ns_blueprint = Blueprint("ns", __name__)
|
ns_blueprint = Blueprint("ns", __name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -72,41 +73,52 @@ def schema(schema="definitions"):
|
|||||||
|
|
||||||
|
|
||||||
def basic_api(f):
|
def basic_api(f):
|
||||||
|
default_params = {
|
||||||
|
'inHeaders': False,
|
||||||
|
'expanded-jsonld': False,
|
||||||
|
'outformat': 'json-ld',
|
||||||
|
'with_parameters': True,
|
||||||
|
}
|
||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
raw_params = get_params(request)
|
raw_params = get_params(request)
|
||||||
|
logger.info('Getting request: {}'.format(raw_params))
|
||||||
headers = {'X-ORIGINAL-PARAMS': json.dumps(raw_params)}
|
headers = {'X-ORIGINAL-PARAMS': json.dumps(raw_params)}
|
||||||
|
params = default_params
|
||||||
|
|
||||||
outformat = 'json-ld'
|
|
||||||
try:
|
try:
|
||||||
print('Getting request:')
|
|
||||||
print(request)
|
|
||||||
params = api.parse_params(raw_params, api.WEB_PARAMS, api.API_PARAMS)
|
params = api.parse_params(raw_params, api.WEB_PARAMS, api.API_PARAMS)
|
||||||
if hasattr(request, 'parameters'):
|
if hasattr(request, 'parameters'):
|
||||||
request.parameters.update(params)
|
request.parameters.update(params)
|
||||||
else:
|
else:
|
||||||
request.parameters = params
|
request.parameters = params
|
||||||
response = f(*args, **kwargs)
|
response = f(*args, **kwargs)
|
||||||
except Error as ex:
|
except (Exception) as ex:
|
||||||
response = ex
|
|
||||||
response.parameters = params
|
|
||||||
logger.error(ex)
|
|
||||||
if current_app.debug:
|
if current_app.debug:
|
||||||
raise
|
raise
|
||||||
|
if not isinstance(ex, Error):
|
||||||
|
msg = "{}:\n\t{}".format(ex,
|
||||||
|
traceback.format_exc())
|
||||||
|
ex = Error(message=msg, status=500)
|
||||||
|
logger.exception('Error returning analysis result')
|
||||||
|
response = ex
|
||||||
|
response.parameters = raw_params
|
||||||
|
logger.error(ex)
|
||||||
|
|
||||||
in_headers = params['inHeaders']
|
if 'parameters' in response and not params['with_parameters']:
|
||||||
expanded = params['expanded-jsonld']
|
del response.parameters
|
||||||
outformat = params['outformat']
|
|
||||||
|
|
||||||
|
logger.info('Response: {}'.format(response))
|
||||||
return response.flask(
|
return response.flask(
|
||||||
in_headers=in_headers,
|
in_headers=params['inHeaders'],
|
||||||
headers=headers,
|
headers=headers,
|
||||||
prefix=url_for('.api_root', _external=True),
|
prefix=url_for('.api_root', _external=True),
|
||||||
context_uri=url_for('api.context',
|
context_uri=url_for('api.context',
|
||||||
entity=type(response).__name__,
|
entity=type(response).__name__,
|
||||||
_external=True),
|
_external=True),
|
||||||
outformat=outformat,
|
outformat=params['outformat'],
|
||||||
expanded=expanded)
|
expanded=params['expanded-jsonld'])
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
@@ -116,21 +128,31 @@ def basic_api(f):
|
|||||||
def api_root():
|
def api_root():
|
||||||
if request.parameters['help']:
|
if request.parameters['help']:
|
||||||
dic = dict(api.API_PARAMS, **api.NIF_PARAMS)
|
dic = dict(api.API_PARAMS, **api.NIF_PARAMS)
|
||||||
|
response = Help(valid_parameters=dic)
|
||||||
|
return response
|
||||||
|
req = api.parse_call(request.parameters)
|
||||||
|
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)
|
response = Help(parameters=dic)
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
req = api.parse_call(request.parameters)
|
params = api.parse_params(request.parameters, api.EVAL_PARAMS)
|
||||||
response = current_app.senpy.analyse(req)
|
response = current_app.senpy.evaluate(params)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@api_blueprint.route('/plugins/', methods=['POST', 'GET'])
|
@api_blueprint.route('/plugins/', methods=['POST', 'GET'])
|
||||||
@basic_api
|
@basic_api
|
||||||
def plugins():
|
def plugins():
|
||||||
sp = current_app.senpy
|
sp = current_app.senpy
|
||||||
ptype = request.parameters.get('plugin_type')
|
params = api.parse_params(request.parameters, api.PLUGINS_PARAMS)
|
||||||
plugins = sp.filter_plugins(plugin_type=ptype)
|
ptype = params.get('plugin_type')
|
||||||
dic = Plugins(plugins=list(plugins.values()))
|
plugins = list(sp.plugins(plugin_type=ptype))
|
||||||
|
dic = Plugins(plugins=plugins)
|
||||||
return dic
|
return dic
|
||||||
|
|
||||||
|
|
||||||
@@ -138,12 +160,13 @@ def plugins():
|
|||||||
@basic_api
|
@basic_api
|
||||||
def plugin(plugin=None):
|
def plugin(plugin=None):
|
||||||
sp = current_app.senpy
|
sp = current_app.senpy
|
||||||
if plugin == 'default' and sp.default_plugin:
|
return sp.get_plugin(plugin)
|
||||||
return sp.default_plugin
|
|
||||||
plugins = sp.filter_plugins(
|
|
||||||
id='plugins/{}'.format(plugin)) or sp.filter_plugins(name=plugin)
|
@api_blueprint.route('/datasets/', methods=['POST','GET'])
|
||||||
if plugins:
|
@basic_api
|
||||||
response = list(plugins.values())[0]
|
def datasets():
|
||||||
else:
|
sp = current_app.senpy
|
||||||
return Error(message="Plugin not found", status=404)
|
datasets = sp.datasets
|
||||||
return response
|
dic = Datasets(datasets = list(datasets.values()))
|
||||||
|
return dic
|
12
senpy/cli.py
12
senpy/cli.py
@@ -28,11 +28,15 @@ def main_function(argv):
|
|||||||
api.API_PARAMS,
|
api.API_PARAMS,
|
||||||
api.NIF_PARAMS)
|
api.NIF_PARAMS)
|
||||||
plugin_folder = params['plugin_folder']
|
plugin_folder = params['plugin_folder']
|
||||||
sp = Senpy(default_plugins=False, plugin_folder=plugin_folder)
|
default_plugins = params.get('default-plugins', False)
|
||||||
|
sp = Senpy(default_plugins=default_plugins, plugin_folder=plugin_folder)
|
||||||
request = api.parse_call(params)
|
request = api.parse_call(params)
|
||||||
algos = request.parameters.get('algorithm', sp.plugins.keys())
|
algos = request.parameters.get('algorithm', None)
|
||||||
|
if algos:
|
||||||
for algo in algos:
|
for algo in algos:
|
||||||
sp.activate_plugin(algo)
|
sp.activate_plugin(algo)
|
||||||
|
else:
|
||||||
|
sp.activate_all()
|
||||||
res = sp.analyse(request)
|
res = sp.analyse(request)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -42,9 +46,9 @@ def main():
|
|||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
res = main_function(sys.argv[1:])
|
res = main_function(sys.argv[1:])
|
||||||
print(res.to_JSON())
|
print(res.serialize())
|
||||||
except Error as err:
|
except Error as err:
|
||||||
print(err.to_JSON())
|
print(err.serialize())
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
from . import models
|
from . import models
|
||||||
from .plugins import default_plugin_type
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -13,10 +12,17 @@ class Client(object):
|
|||||||
def analyse(self, input, method='GET', **kwargs):
|
def analyse(self, input, method='GET', **kwargs):
|
||||||
return self.request('/', method=method, input=input, **kwargs)
|
return self.request('/', method=method, input=input, **kwargs)
|
||||||
|
|
||||||
def plugins(self, ptype=default_plugin_type):
|
def evaluate(self, input, method='GET', **kwargs):
|
||||||
resp = self.request(path='/plugins', plugin_type=ptype).plugins
|
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}
|
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):
|
def request(self, path=None, method='GET', **params):
|
||||||
url = '{}{}'.format(self.endpoint, path)
|
url = '{}{}'.format(self.endpoint, path)
|
||||||
response = requests.request(method=method, url=url, params=params)
|
response = requests.request(method=method, url=url, params=params)
|
||||||
|
@@ -6,42 +6,58 @@ from future import standard_library
|
|||||||
standard_library.install_aliases()
|
standard_library.install_aliases()
|
||||||
|
|
||||||
from . import plugins, api
|
from . import plugins, api
|
||||||
from .plugins import SenpyPlugin
|
from .plugins import Plugin, evaluate
|
||||||
from .models import Error
|
from .models import Error, AggregatedEvaluation
|
||||||
from .blueprints import api_blueprint, demo_blueprint, ns_blueprint
|
from .blueprints import api_blueprint, demo_blueprint, ns_blueprint
|
||||||
|
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import copy
|
import copy
|
||||||
|
import errno
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from gsitk.datasets.datasets import DatasetManager
|
||||||
|
GSITK_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
logger.warn('GSITK is not installed. Some functions will be unavailable.')
|
||||||
|
GSITK_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
class Senpy(object):
|
class Senpy(object):
|
||||||
""" Default Senpy extension for Flask """
|
""" Default Senpy extension for Flask """
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
app=None,
|
app=None,
|
||||||
plugin_folder=".",
|
plugin_folder=".",
|
||||||
|
data_folder=None,
|
||||||
default_plugins=False):
|
default_plugins=False):
|
||||||
self.app = app
|
|
||||||
self._search_folders = set()
|
|
||||||
self._plugin_list = []
|
|
||||||
self._outdated = True
|
|
||||||
self._default = None
|
|
||||||
|
|
||||||
|
default_data = os.path.join(os.getcwd(), 'senpy_data')
|
||||||
|
self.data_folder = data_folder or os.environ.get('SENPY_DATA', default_data)
|
||||||
|
try:
|
||||||
|
os.makedirs(self.data_folder)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno == errno.EEXIST:
|
||||||
|
logger.debug('Data folder exists: {}'.format(self.data_folder))
|
||||||
|
else: # pragma: no cover
|
||||||
|
raise
|
||||||
|
|
||||||
|
self._default = None
|
||||||
|
self._plugins = {}
|
||||||
|
if plugin_folder:
|
||||||
self.add_folder(plugin_folder)
|
self.add_folder(plugin_folder)
|
||||||
|
|
||||||
if default_plugins:
|
if default_plugins:
|
||||||
self.add_folder('plugins', from_root=True)
|
self.add_folder('plugins', from_root=True)
|
||||||
else:
|
else:
|
||||||
# Add only conversion plugins
|
# Add only conversion plugins
|
||||||
self.add_folder(os.path.join('plugins', 'conversion'),
|
self.add_folder(os.path.join('plugins', 'conversion'),
|
||||||
from_root=True)
|
from_root=True)
|
||||||
|
self.app = app
|
||||||
if app is not None:
|
if app is not None:
|
||||||
self.init_app(app)
|
self.init_app(app)
|
||||||
|
|
||||||
@@ -56,21 +72,52 @@ class Senpy(object):
|
|||||||
# otherwise fall back to the request context
|
# otherwise fall back to the request context
|
||||||
if hasattr(app, 'teardown_appcontext'):
|
if hasattr(app, 'teardown_appcontext'):
|
||||||
app.teardown_appcontext(self.teardown)
|
app.teardown_appcontext(self.teardown)
|
||||||
else:
|
else: # pragma: no cover
|
||||||
app.teardown_request(self.teardown)
|
app.teardown_request(self.teardown)
|
||||||
app.register_blueprint(api_blueprint, url_prefix="/api")
|
app.register_blueprint(api_blueprint, url_prefix="/api")
|
||||||
app.register_blueprint(ns_blueprint, url_prefix="/ns")
|
app.register_blueprint(ns_blueprint, url_prefix="/ns")
|
||||||
app.register_blueprint(demo_blueprint, url_prefix="/")
|
app.register_blueprint(demo_blueprint, url_prefix="/")
|
||||||
|
|
||||||
|
def add_plugin(self, plugin):
|
||||||
|
self._plugins[plugin.name.lower()] = plugin
|
||||||
|
|
||||||
|
def delete_plugin(self, plugin):
|
||||||
|
del self._plugins[plugin.name.lower()]
|
||||||
|
|
||||||
|
def plugins(self, **kwargs):
|
||||||
|
""" Return the plugins registered for a given application. Filtered by criteria """
|
||||||
|
return list(plugins.pfilter(self._plugins, **kwargs))
|
||||||
|
|
||||||
|
def get_plugin(self, name, default=None):
|
||||||
|
if name == 'default':
|
||||||
|
return self.default_plugin
|
||||||
|
plugin = name.lower()
|
||||||
|
if plugin in self._plugins:
|
||||||
|
return self._plugins[plugin]
|
||||||
|
|
||||||
|
results = self.plugins(id='plugins/{}'.format(name))
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
return Error(message="Plugin not found", status=404)
|
||||||
|
return results[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def analysis_plugins(self):
|
||||||
|
""" Return only the analysis plugins """
|
||||||
|
return self.plugins(plugin_type='analysisPlugin')
|
||||||
|
|
||||||
def add_folder(self, folder, from_root=False):
|
def add_folder(self, folder, from_root=False):
|
||||||
|
""" Find plugins in this folder and add them to this instance """
|
||||||
if from_root:
|
if from_root:
|
||||||
folder = os.path.join(os.path.dirname(__file__), folder)
|
folder = os.path.join(os.path.dirname(__file__), folder)
|
||||||
logger.debug("Adding folder: %s", folder)
|
logger.debug("Adding folder: %s", folder)
|
||||||
if os.path.isdir(folder):
|
if os.path.isdir(folder):
|
||||||
self._search_folders.add(folder)
|
new_plugins = plugins.from_folder([folder],
|
||||||
self._outdated = True
|
data_folder=self.data_folder)
|
||||||
|
for plugin in new_plugins:
|
||||||
|
self.add_plugin(plugin)
|
||||||
else:
|
else:
|
||||||
logger.debug("Not a folder: %s", folder)
|
raise AttributeError("Not a folder or does not exist: %s", folder)
|
||||||
|
|
||||||
def _get_plugins(self, request):
|
def _get_plugins(self, request):
|
||||||
if not self.analysis_plugins:
|
if not self.analysis_plugins:
|
||||||
@@ -89,14 +136,16 @@ class Senpy(object):
|
|||||||
|
|
||||||
plugins = list()
|
plugins = list()
|
||||||
for algo in algos:
|
for algo in algos:
|
||||||
if algo not in self.plugins:
|
algo = algo.lower()
|
||||||
logger.debug(("The algorithm '{}' is not valid\n"
|
if algo not in self._plugins:
|
||||||
|
msg = ("The algorithm '{}' is not valid\n"
|
||||||
"Valid algorithms: {}").format(algo,
|
"Valid algorithms: {}").format(algo,
|
||||||
self.plugins.keys()))
|
self._plugins.keys())
|
||||||
|
logger.debug(msg)
|
||||||
raise Error(
|
raise Error(
|
||||||
status=404,
|
status=404,
|
||||||
message="The algorithm '{}' is not valid".format(algo))
|
message=msg)
|
||||||
plugins.append(self.plugins[algo])
|
plugins.append(self._plugins[algo])
|
||||||
return plugins
|
return plugins
|
||||||
|
|
||||||
def _process_entries(self, entries, req, plugins):
|
def _process_entries(self, entries, req, plugins):
|
||||||
@@ -110,7 +159,7 @@ class Senpy(object):
|
|||||||
return
|
return
|
||||||
plugin = plugins[0]
|
plugin = plugins[0]
|
||||||
self._activate(plugin) # Make sure the plugin is activated
|
self._activate(plugin) # Make sure the plugin is activated
|
||||||
specific_params = api.get_extra_params(req, plugin)
|
specific_params = api.parse_extra_params(req, plugin)
|
||||||
req.analysis.append({'plugin': plugin,
|
req.analysis.append({'plugin': plugin,
|
||||||
'parameters': specific_params})
|
'parameters': specific_params})
|
||||||
results = plugin.analyse_entries(entries, specific_params)
|
results = plugin.analyse_entries(entries, specific_params)
|
||||||
@@ -118,7 +167,7 @@ class Senpy(object):
|
|||||||
yield i
|
yield i
|
||||||
|
|
||||||
def install_deps(self):
|
def install_deps(self):
|
||||||
for plugin in self.filter_plugins(is_activated=True):
|
for plugin in self.plugins(is_activated=True):
|
||||||
plugins.install_deps(plugin)
|
plugins.install_deps(plugin)
|
||||||
|
|
||||||
def analyse(self, request):
|
def analyse(self, request):
|
||||||
@@ -128,7 +177,6 @@ class Senpy(object):
|
|||||||
by api.parse_call().
|
by api.parse_call().
|
||||||
"""
|
"""
|
||||||
logger.debug("analysing request: {}".format(request))
|
logger.debug("analysing request: {}".format(request))
|
||||||
try:
|
|
||||||
entries = request.entries
|
entries = request.entries
|
||||||
request.entries = []
|
request.entries = []
|
||||||
plugins = self._get_plugins(request)
|
plugins = self._get_plugins(request)
|
||||||
@@ -136,28 +184,67 @@ class Senpy(object):
|
|||||||
for i in self._process_entries(entries, results, plugins):
|
for i in self._process_entries(entries, results, plugins):
|
||||||
results.entries.append(i)
|
results.entries.append(i)
|
||||||
self.convert_emotions(results)
|
self.convert_emotions(results)
|
||||||
if 'with_parameters' not in results.parameters:
|
|
||||||
del results.parameters
|
|
||||||
logger.debug("Returning analysis result: {}".format(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]
|
results.analysis = [i['plugin'].id for i in results.analysis]
|
||||||
return results
|
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 = DatasetManager()
|
||||||
|
datasets = dm.prepare_datasets(datasets_name)
|
||||||
|
return datasets
|
||||||
|
|
||||||
|
@property
|
||||||
|
def datasets(self):
|
||||||
|
if not GSITK_AVAILABLE:
|
||||||
|
raise Exception('GSITK is not available. Install it to use this function.')
|
||||||
|
self._dataset_list = {}
|
||||||
|
dm = DatasetManager()
|
||||||
|
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):
|
||||||
|
if not GSITK_AVAILABLE:
|
||||||
|
raise Exception('GSITK is not available. Install it to use this function.')
|
||||||
|
logger.debug("evaluating request: {}".format(params))
|
||||||
|
results = AggregatedEvaluation()
|
||||||
|
results.parameters = params
|
||||||
|
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):
|
def _conversion_candidates(self, fromModel, toModel):
|
||||||
candidates = self.filter_plugins(plugin_type='emotionConversionPlugin')
|
candidates = self.plugins(plugin_type='emotionConversionPlugin')
|
||||||
for name, candidate in candidates.items():
|
for candidate in candidates:
|
||||||
for pair in candidate.onyx__doesConversion:
|
for pair in candidate.onyx__doesConversion:
|
||||||
logging.debug(pair)
|
logging.debug(pair)
|
||||||
|
|
||||||
if pair['onyx:conversionFrom'] == fromModel \
|
if pair['onyx:conversionFrom'] == fromModel \
|
||||||
and pair['onyx:conversionTo'] == toModel:
|
and pair['onyx:conversionTo'] == toModel:
|
||||||
# logging.debug('Found candidate: {}'.format(candidate))
|
|
||||||
yield candidate
|
yield candidate
|
||||||
|
|
||||||
def convert_emotions(self, resp):
|
def convert_emotions(self, resp):
|
||||||
@@ -184,7 +271,8 @@ class Senpy(object):
|
|||||||
logger.debug('Analysis plugin {} uses model: {}'.format(plugin.id, fromModel))
|
logger.debug('Analysis plugin {} uses model: {}'.format(plugin.id, fromModel))
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
e = Error(('No conversion plugin found for: '
|
e = Error(('No conversion plugin found for: '
|
||||||
'{} -> {}'.format(fromModel, toModel)))
|
'{} -> {}'.format(fromModel, toModel)),
|
||||||
|
status=404)
|
||||||
e.original_response = resp
|
e.original_response = resp
|
||||||
e.parameters = params
|
e.parameters = params
|
||||||
raise e
|
raise e
|
||||||
@@ -210,36 +298,40 @@ class Senpy(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def default_plugin(self):
|
def default_plugin(self):
|
||||||
candidate = self._default
|
if not self._default or not self._default.is_activated:
|
||||||
if not candidate:
|
candidates = self.plugins(plugin_type='analysisPlugin',
|
||||||
candidates = self.filter_plugins(plugin_type='analysisPlugin',
|
|
||||||
is_activated=True)
|
is_activated=True)
|
||||||
if len(candidates) > 0:
|
if len(candidates) > 0:
|
||||||
candidate = list(candidates.values())[0]
|
self._default = candidates[0]
|
||||||
logger.debug("Default: {}".format(candidate))
|
else:
|
||||||
return candidate
|
self._default = None
|
||||||
|
logger.debug("Default: {}".format(self._default))
|
||||||
|
return self._default
|
||||||
|
|
||||||
@default_plugin.setter
|
@default_plugin.setter
|
||||||
def default_plugin(self, value):
|
def default_plugin(self, value):
|
||||||
if isinstance(value, SenpyPlugin):
|
if isinstance(value, Plugin):
|
||||||
|
if not value.is_activated:
|
||||||
|
raise AttributeError('The default plugin has to be activated.')
|
||||||
self._default = value
|
self._default = value
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self._default = self.plugins[value]
|
self._default = self._plugins[value.lower()]
|
||||||
|
|
||||||
def activate_all(self, sync=True):
|
def activate_all(self, sync=True):
|
||||||
ps = []
|
ps = []
|
||||||
for plug in self.plugins.keys():
|
for plug in self._plugins.keys():
|
||||||
ps.append(self.activate_plugin(plug, sync=sync))
|
ps.append(self.activate_plugin(plug, sync=sync))
|
||||||
return ps
|
return ps
|
||||||
|
|
||||||
def deactivate_all(self, sync=True):
|
def deactivate_all(self, sync=True):
|
||||||
ps = []
|
ps = []
|
||||||
for plug in self.plugins.keys():
|
for plug in self._plugins.keys():
|
||||||
ps.append(self.deactivate_plugin(plug, sync=sync))
|
ps.append(self.deactivate_plugin(plug, sync=sync))
|
||||||
return ps
|
return ps
|
||||||
|
|
||||||
def _set_active(self, plugin, active=True, *args, **kwargs):
|
def _set_active(self, plugin, active=True, *args, **kwargs):
|
||||||
''' We're using a variable in the plugin itself to activate/deactive plugins.\
|
''' We're using a variable in the plugin itself to activate/deactivate plugins.\
|
||||||
Note that plugins may activate themselves by setting this variable.
|
Note that plugins may activate themselves by setting this variable.
|
||||||
'''
|
'''
|
||||||
plugin.is_activated = active
|
plugin.is_activated = active
|
||||||
@@ -249,24 +341,18 @@ class Senpy(object):
|
|||||||
with plugin._lock:
|
with plugin._lock:
|
||||||
if plugin.is_activated:
|
if plugin.is_activated:
|
||||||
return
|
return
|
||||||
try:
|
|
||||||
plugin.activate()
|
plugin.activate()
|
||||||
msg = "Plugin activated: {}".format(plugin.name)
|
msg = "Plugin activated: {}".format(plugin.name)
|
||||||
logger.info(msg)
|
logger.info(msg)
|
||||||
success = True
|
success = True
|
||||||
self._set_active(plugin, success)
|
self._set_active(plugin, success)
|
||||||
except Exception as ex:
|
|
||||||
msg = "Error activating plugin {} - {} : \n\t{}".format(
|
|
||||||
plugin.name, ex, traceback.format_exc())
|
|
||||||
logger.error(msg)
|
|
||||||
raise Error(msg)
|
|
||||||
|
|
||||||
def activate_plugin(self, plugin_name, sync=True):
|
def activate_plugin(self, plugin_name, sync=True):
|
||||||
try:
|
plugin_name = plugin_name.lower()
|
||||||
plugin = self.plugins[plugin_name]
|
if plugin_name not in self._plugins:
|
||||||
except KeyError:
|
|
||||||
raise Error(
|
raise Error(
|
||||||
message="Plugin not found: {}".format(plugin_name), status=404)
|
message="Plugin not found: {}".format(plugin_name), status=404)
|
||||||
|
plugin = self._plugins[plugin_name]
|
||||||
|
|
||||||
logger.info("Activating plugin: {}".format(plugin.name))
|
logger.info("Activating plugin: {}".format(plugin.name))
|
||||||
|
|
||||||
@@ -281,20 +367,15 @@ class Senpy(object):
|
|||||||
with plugin._lock:
|
with plugin._lock:
|
||||||
if not plugin.is_activated:
|
if not plugin.is_activated:
|
||||||
return
|
return
|
||||||
try:
|
|
||||||
plugin.deactivate()
|
plugin.deactivate()
|
||||||
logger.info("Plugin deactivated: {}".format(plugin.name))
|
logger.info("Plugin deactivated: {}".format(plugin.name))
|
||||||
except Exception as ex:
|
|
||||||
logger.error(
|
|
||||||
"Error deactivating plugin {}: {}".format(plugin.name, ex))
|
|
||||||
logger.error("Trace: {}".format(traceback.format_exc()))
|
|
||||||
|
|
||||||
def deactivate_plugin(self, plugin_name, sync=True):
|
def deactivate_plugin(self, plugin_name, sync=True):
|
||||||
try:
|
plugin_name = plugin_name.lower()
|
||||||
plugin = self.plugins[plugin_name]
|
if plugin_name not in self._plugins:
|
||||||
except KeyError:
|
|
||||||
raise Error(
|
raise Error(
|
||||||
message="Plugin not found: {}".format(plugin_name), status=404)
|
message="Plugin not found: {}".format(plugin_name), status=404)
|
||||||
|
plugin = self._plugins[plugin_name]
|
||||||
|
|
||||||
self._set_active(plugin, False)
|
self._set_active(plugin, False)
|
||||||
|
|
||||||
@@ -307,19 +388,3 @@ class Senpy(object):
|
|||||||
|
|
||||||
def teardown(self, exception):
|
def teardown(self, exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
|
||||||
def plugins(self):
|
|
||||||
""" Return the plugins registered for a given application. """
|
|
||||||
if self._outdated:
|
|
||||||
self._plugin_list = plugins.load_plugins(self._search_folders)
|
|
||||||
self._outdated = False
|
|
||||||
return self._plugin_list
|
|
||||||
|
|
||||||
def filter_plugins(self, **kwargs):
|
|
||||||
return plugins.pfilter(self.plugins, **kwargs)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def analysis_plugins(self):
|
|
||||||
""" Return only the analysis plugins """
|
|
||||||
return self.filter_plugins(plugin_type='analysisPlugin')
|
|
||||||
|
257
senpy/meta.py
Normal file
257
senpy/meta.py
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
'''
|
||||||
|
Meta-programming for the models.
|
||||||
|
'''
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import jsonschema
|
||||||
|
import inspect
|
||||||
|
import copy
|
||||||
|
|
||||||
|
from abc import ABCMeta
|
||||||
|
from collections import MutableMapping, namedtuple
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMeta(ABCMeta):
|
||||||
|
'''
|
||||||
|
Metaclass for models. It extracts the default values for the fields in
|
||||||
|
the model.
|
||||||
|
|
||||||
|
For instance, instances of the following class wouldn't need to mark
|
||||||
|
their version or description on initialization:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class MyPlugin(Plugin):
|
||||||
|
version=0.3
|
||||||
|
description='A dull plugin'
|
||||||
|
|
||||||
|
|
||||||
|
Note that these operations could be included in the __init__ of the
|
||||||
|
class, but it would be very inefficient.
|
||||||
|
'''
|
||||||
|
_subtypes = {}
|
||||||
|
|
||||||
|
def __new__(mcs, name, bases, attrs, **kwargs):
|
||||||
|
register_afterwards = False
|
||||||
|
defaults = {}
|
||||||
|
|
||||||
|
attrs = mcs.expand_with_schema(name, attrs)
|
||||||
|
if 'schema' in attrs:
|
||||||
|
register_afterwards = True
|
||||||
|
for base in bases:
|
||||||
|
if hasattr(base, '_defaults'):
|
||||||
|
defaults.update(getattr(base, '_defaults'))
|
||||||
|
|
||||||
|
info, rest = mcs.split_attrs(attrs)
|
||||||
|
|
||||||
|
for i in list(info.keys()):
|
||||||
|
if isinstance(info[i], _Alias):
|
||||||
|
fget, fset, fdel = make_property(info[i].indict)
|
||||||
|
rest[i] = property(fget=fget, fset=fset, fdel=fdel)
|
||||||
|
else:
|
||||||
|
defaults[i] = info[i]
|
||||||
|
|
||||||
|
rest['_defaults'] = defaults
|
||||||
|
|
||||||
|
cls = super(BaseMeta, mcs).__new__(mcs, name, tuple(bases), rest)
|
||||||
|
|
||||||
|
if register_afterwards:
|
||||||
|
mcs.register(cls, defaults['@type'])
|
||||||
|
return cls
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register(mcs, rsubclass, rtype=None):
|
||||||
|
mcs._subtypes[rtype or rsubclass.__name__] = rsubclass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def expand_with_schema(name, attrs):
|
||||||
|
if 'schema' in attrs: # Schema specified by name
|
||||||
|
schema_file = '{}.json'.format(attrs['schema'])
|
||||||
|
elif 'schema_file' in attrs:
|
||||||
|
schema_file = attrs['schema_file']
|
||||||
|
del attrs['schema_file']
|
||||||
|
else:
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
if '/' not in 'schema_file':
|
||||||
|
thisdir = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
schema_file = os.path.join(thisdir,
|
||||||
|
'schemas',
|
||||||
|
schema_file)
|
||||||
|
|
||||||
|
schema_path = 'file://' + schema_file
|
||||||
|
|
||||||
|
with open(schema_file) as f:
|
||||||
|
schema = json.load(f)
|
||||||
|
|
||||||
|
resolver = jsonschema.RefResolver(schema_path, schema)
|
||||||
|
attrs['@type'] = "".join((name[0].lower(), name[1:]))
|
||||||
|
attrs['_schema_file'] = schema_file
|
||||||
|
attrs['schema'] = schema
|
||||||
|
attrs['_validator'] = jsonschema.Draft4Validator(schema, resolver=resolver)
|
||||||
|
|
||||||
|
schema_defaults = BaseMeta.get_defaults(attrs['schema'])
|
||||||
|
attrs.update(schema_defaults)
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_func(v):
|
||||||
|
return inspect.isroutine(v) or inspect.ismethod(v) or \
|
||||||
|
inspect.ismodule(v) or isinstance(v, property)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_internal(k):
|
||||||
|
return k[0] == '_' or k == 'schema' or k == 'data'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_key(key):
|
||||||
|
if key[0] != '_':
|
||||||
|
key = key.replace("__", ":", 1)
|
||||||
|
return key
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def split_attrs(attrs):
|
||||||
|
'''
|
||||||
|
Extract the attributes of the class.
|
||||||
|
|
||||||
|
This allows adding default values in the class definition.
|
||||||
|
e.g.:
|
||||||
|
'''
|
||||||
|
isattr = {}
|
||||||
|
rest = {}
|
||||||
|
for key, value in attrs.items():
|
||||||
|
if not (BaseMeta.is_internal(key)) and (not BaseMeta.is_func(value)):
|
||||||
|
isattr[key] = value
|
||||||
|
else:
|
||||||
|
rest[key] = value
|
||||||
|
return isattr, rest
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_defaults(schema):
|
||||||
|
temp = {}
|
||||||
|
for obj in [
|
||||||
|
schema,
|
||||||
|
] + schema.get('allOf', []):
|
||||||
|
for k, v in obj.get('properties', {}).items():
|
||||||
|
if 'default' in v and k not in temp:
|
||||||
|
temp[k] = v['default']
|
||||||
|
return temp
|
||||||
|
|
||||||
|
|
||||||
|
def make_property(key):
|
||||||
|
|
||||||
|
def fget(self):
|
||||||
|
return self[key]
|
||||||
|
|
||||||
|
def fdel(self):
|
||||||
|
del self[key]
|
||||||
|
|
||||||
|
def fset(self, value):
|
||||||
|
self[key] = value
|
||||||
|
|
||||||
|
return fget, fset, fdel
|
||||||
|
|
||||||
|
|
||||||
|
class CustomDict(MutableMapping, object):
|
||||||
|
'''
|
||||||
|
A dictionary whose elements can also be accessed as attributes. Since some
|
||||||
|
characters are not valid in the dot-notation, the attribute names also
|
||||||
|
converted. e.g.:
|
||||||
|
|
||||||
|
> d = CustomDict()
|
||||||
|
> d.key = d['ns:name'] = 1
|
||||||
|
> d.key == d['key']
|
||||||
|
True
|
||||||
|
> d.ns__name == d['ns:name']
|
||||||
|
'''
|
||||||
|
|
||||||
|
_defaults = {}
|
||||||
|
_map_attr_key = {'id': '@id'}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(CustomDict, self).__init__()
|
||||||
|
for k, v in self._defaults.items():
|
||||||
|
self[k] = copy.copy(v)
|
||||||
|
for arg in args:
|
||||||
|
self.update(arg)
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
self[self._attr_to_key(k)] = v
|
||||||
|
return self
|
||||||
|
|
||||||
|
def serializable(self):
|
||||||
|
def ser_or_down(item):
|
||||||
|
if hasattr(item, 'serializable'):
|
||||||
|
return item.serializable()
|
||||||
|
elif isinstance(item, dict):
|
||||||
|
temp = dict()
|
||||||
|
for kp in item:
|
||||||
|
vp = item[kp]
|
||||||
|
temp[kp] = ser_or_down(vp)
|
||||||
|
return temp
|
||||||
|
elif isinstance(item, list) or isinstance(item, set):
|
||||||
|
return list(ser_or_down(i) for i in item)
|
||||||
|
else:
|
||||||
|
return item
|
||||||
|
|
||||||
|
return ser_or_down(self.as_dict())
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
key = self._key_to_attr(key)
|
||||||
|
return self.__dict__[key]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
'''Do not insert data directly, there might be a property in that key. '''
|
||||||
|
key = self._key_to_attr(key)
|
||||||
|
return setattr(self, key, value)
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
return {self._attr_to_key(k): v for k, v in self.__dict__.items()
|
||||||
|
if not self._internal_key(k)}
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return (k for k in self.__dict__ if not self._internal_key(k))
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.__dict__)
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
del self.__dict__[key]
|
||||||
|
|
||||||
|
def update(self, other):
|
||||||
|
for k, v in other.items():
|
||||||
|
self[k] = v
|
||||||
|
|
||||||
|
def _attr_to_key(self, key):
|
||||||
|
key = key.replace("__", ":", 1)
|
||||||
|
key = self._map_attr_key.get(key, key)
|
||||||
|
return key
|
||||||
|
|
||||||
|
def _key_to_attr(self, key):
|
||||||
|
if self._internal_key(key):
|
||||||
|
return key
|
||||||
|
key = key.replace(":", "__", 1)
|
||||||
|
return key
|
||||||
|
|
||||||
|
def __getattr__(self, key):
|
||||||
|
try:
|
||||||
|
return self.__dict__[self._attr_to_key(key)]
|
||||||
|
except KeyError:
|
||||||
|
raise AttributeError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _internal_key(key):
|
||||||
|
return key[0] == '_'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.serializable())
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self.serializable())
|
||||||
|
|
||||||
|
|
||||||
|
_Alias = namedtuple('Alias', 'indict')
|
||||||
|
|
||||||
|
|
||||||
|
def alias(key):
|
||||||
|
return _Alias(key)
|
329
senpy/models.py
329
senpy/models.py
@@ -6,24 +6,30 @@ For compatibility with Py3 and for easier debugging, this new version drops
|
|||||||
introspection and adds all arguments to the models.
|
introspection and adds all arguments to the models.
|
||||||
'''
|
'''
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
from six import string_types
|
from future import standard_library
|
||||||
|
standard_library.install_aliases()
|
||||||
|
|
||||||
|
from future.utils import with_metaclass
|
||||||
|
from past.builtins import basestring
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import jsonref
|
import jsonref
|
||||||
import jsonschema
|
|
||||||
|
|
||||||
from flask import Response as FlaskResponse
|
from flask import Response as FlaskResponse
|
||||||
from pyld import jsonld
|
from pyld import jsonld
|
||||||
|
|
||||||
from rdflib import Graph
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
logging.getLogger('rdflib').setLevel(logging.WARN)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
from rdflib import Graph
|
||||||
|
|
||||||
|
|
||||||
|
from .meta import BaseMeta, CustomDict, alias
|
||||||
|
|
||||||
DEFINITIONS_FILE = 'definitions.json'
|
DEFINITIONS_FILE = 'definitions.json'
|
||||||
CONTEXT_PATH = os.path.join(
|
CONTEXT_PATH = os.path.join(
|
||||||
os.path.dirname(os.path.realpath(__file__)), 'schemas', 'context.jsonld')
|
os.path.dirname(os.path.realpath(__file__)), 'schemas', 'context.jsonld')
|
||||||
@@ -45,38 +51,96 @@ def read_schema(schema_file, absolute=False):
|
|||||||
return jsonref.load(f, base_uri=schema_uri)
|
return jsonref.load(f, base_uri=schema_uri)
|
||||||
|
|
||||||
|
|
||||||
base_schema = read_schema(DEFINITIONS_FILE)
|
def load_context(context):
|
||||||
|
|
||||||
|
|
||||||
class Context(dict):
|
|
||||||
@staticmethod
|
|
||||||
def load(context):
|
|
||||||
logging.debug('Loading context: {}'.format(context))
|
logging.debug('Loading context: {}'.format(context))
|
||||||
if not context:
|
if not context:
|
||||||
return context
|
return context
|
||||||
elif isinstance(context, list):
|
elif isinstance(context, list):
|
||||||
contexts = []
|
contexts = []
|
||||||
for c in context:
|
for c in context:
|
||||||
contexts.append(Context.load(c))
|
contexts.append(load_context(c))
|
||||||
return contexts
|
return contexts
|
||||||
elif isinstance(context, dict):
|
elif isinstance(context, dict):
|
||||||
return Context(context)
|
return dict(context)
|
||||||
elif isinstance(context, string_types):
|
elif isinstance(context, basestring):
|
||||||
try:
|
try:
|
||||||
with open(context) as f:
|
with open(context) as f:
|
||||||
return Context(json.loads(f.read()))
|
return dict(json.loads(f.read()))
|
||||||
except IOError:
|
except IOError:
|
||||||
return context
|
return context
|
||||||
else:
|
else:
|
||||||
raise AttributeError('Please, provide a valid context')
|
raise AttributeError('Please, provide a valid context')
|
||||||
|
|
||||||
|
|
||||||
base_context = Context.load(CONTEXT_PATH)
|
base_context = load_context(CONTEXT_PATH)
|
||||||
|
|
||||||
|
|
||||||
class SenpyMixin(object):
|
def register(rsubclass, rtype=None):
|
||||||
|
BaseMeta.register(rsubclass, rtype)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
||||||
|
'''
|
||||||
|
Entities of the base model are a special kind of dictionary that emulates
|
||||||
|
a JSON-LD object. The structure of the dictionary is checked via JSON-schema.
|
||||||
|
For convenience, the values can also be accessed as attributes
|
||||||
|
(a la Javascript). e.g.:
|
||||||
|
|
||||||
|
>>> myobject.key == myobject['key']
|
||||||
|
True
|
||||||
|
>>> myobject.ns__name == myobject['ns:name']
|
||||||
|
True
|
||||||
|
|
||||||
|
Additionally, subclasses of this class can specify default values for their
|
||||||
|
instances. These defaults are inherited by subclasses. e.g.:
|
||||||
|
|
||||||
|
>>> class NewModel(BaseModel):
|
||||||
|
... mydefault = 5
|
||||||
|
>>> n1 = NewModel()
|
||||||
|
>>> n1['mydefault'] == 5
|
||||||
|
True
|
||||||
|
>>> n1.mydefault = 3
|
||||||
|
>>> n1['mydefault'] = 3
|
||||||
|
True
|
||||||
|
>>> n2 = NewModel()
|
||||||
|
>>> n2 == 5
|
||||||
|
True
|
||||||
|
>>> class SubModel(NewModel):
|
||||||
|
pass
|
||||||
|
>>> subn = SubModel()
|
||||||
|
>>> subn.mydefault == 5
|
||||||
|
True
|
||||||
|
|
||||||
|
Lastly, every subclass that also specifies a schema will get registered, so it
|
||||||
|
is possible to deserialize JSON and get the right type.
|
||||||
|
i.e. to recover an instance of the original class from a plain JSON.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
schema_file = DEFINITIONS_FILE
|
||||||
_context = base_context["@context"]
|
_context = base_context["@context"]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
auto_id = kwargs.pop('_auto_id', True)
|
||||||
|
|
||||||
|
super(BaseModel, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if auto_id:
|
||||||
|
self.id
|
||||||
|
|
||||||
|
if '@type' not in self:
|
||||||
|
logger.warn('Created an instance of an unknown model')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
if '@id' not in self:
|
||||||
|
self['@id'] = ':{}_{}'.format(type(self).__name__, time.time())
|
||||||
|
return self['@id']
|
||||||
|
|
||||||
|
@id.setter
|
||||||
|
def id(self, value):
|
||||||
|
self['@id'] = value
|
||||||
|
|
||||||
def flask(self,
|
def flask(self,
|
||||||
in_headers=True,
|
in_headers=True,
|
||||||
headers=None,
|
headers=None,
|
||||||
@@ -102,7 +166,7 @@ class SenpyMixin(object):
|
|||||||
})
|
})
|
||||||
return FlaskResponse(
|
return FlaskResponse(
|
||||||
response=content,
|
response=content,
|
||||||
status=getattr(self, "status", 200),
|
status=self.get('status', 200),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
mimetype=mimetype)
|
mimetype=mimetype)
|
||||||
|
|
||||||
@@ -130,25 +194,8 @@ class SenpyMixin(object):
|
|||||||
else:
|
else:
|
||||||
return content
|
return content
|
||||||
|
|
||||||
def serializable(self):
|
|
||||||
def ser_or_down(item):
|
|
||||||
if hasattr(item, 'serializable'):
|
|
||||||
return item.serializable()
|
|
||||||
elif isinstance(item, dict):
|
|
||||||
temp = dict()
|
|
||||||
for kp in item:
|
|
||||||
vp = item[kp]
|
|
||||||
temp[kp] = ser_or_down(vp)
|
|
||||||
return temp
|
|
||||||
elif isinstance(item, list) or isinstance(item, set):
|
|
||||||
return list(ser_or_down(i) for i in item)
|
|
||||||
else:
|
|
||||||
return item
|
|
||||||
|
|
||||||
return ser_or_down(self._plain_dict())
|
|
||||||
|
|
||||||
def jsonld(self,
|
def jsonld(self,
|
||||||
with_context=True,
|
with_context=False,
|
||||||
context_uri=None,
|
context_uri=None,
|
||||||
prefix=None,
|
prefix=None,
|
||||||
expanded=False):
|
expanded=False):
|
||||||
@@ -172,10 +219,6 @@ class SenpyMixin(object):
|
|||||||
del result['@context']
|
del result['@context']
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def to_JSON(self, *args, **kwargs):
|
|
||||||
js = json.dumps(self.jsonld(*args, **kwargs), indent=4, sort_keys=True)
|
|
||||||
return js
|
|
||||||
|
|
||||||
def validate(self, obj=None):
|
def validate(self, obj=None):
|
||||||
if not obj:
|
if not obj:
|
||||||
obj = self
|
obj = self
|
||||||
@@ -183,86 +226,22 @@ class SenpyMixin(object):
|
|||||||
obj = obj.jsonld()
|
obj = obj.jsonld()
|
||||||
self._validator.validate(obj)
|
self._validator.validate(obj)
|
||||||
|
|
||||||
def __str__(self):
|
def prov(self, another):
|
||||||
return str(self.serialize())
|
self['prov:wasGeneratedBy'] = another.id
|
||||||
|
|
||||||
|
|
||||||
class BaseModel(SenpyMixin, dict):
|
def subtypes():
|
||||||
|
return BaseMeta._subtypes
|
||||||
schema = base_schema
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
if 'id' in kwargs:
|
|
||||||
self.id = kwargs.pop('id')
|
|
||||||
elif kwargs.pop('_auto_id', True):
|
|
||||||
self.id = '_:{}_{}'.format(type(self).__name__, time.time())
|
|
||||||
temp = dict(*args, **kwargs)
|
|
||||||
|
|
||||||
for obj in [
|
|
||||||
self.schema,
|
|
||||||
] + self.schema.get('allOf', []):
|
|
||||||
for k, v in obj.get('properties', {}).items():
|
|
||||||
if 'default' in v and k not in temp:
|
|
||||||
temp[k] = copy.deepcopy(v['default'])
|
|
||||||
|
|
||||||
for i in temp:
|
|
||||||
nk = self._get_key(i)
|
|
||||||
if nk != i:
|
|
||||||
temp[nk] = temp[i]
|
|
||||||
del temp[i]
|
|
||||||
try:
|
|
||||||
temp['@type'] = getattr(self, '@type')
|
|
||||||
except AttributeError:
|
|
||||||
logger.warn('Creating an instance of an unknown model')
|
|
||||||
|
|
||||||
super(BaseModel, self).__init__(temp)
|
|
||||||
|
|
||||||
def _get_key(self, key):
|
|
||||||
if key is 'id':
|
|
||||||
key = '@id'
|
|
||||||
key = key.replace("__", ":", 1)
|
|
||||||
return key
|
|
||||||
|
|
||||||
def __delitem__(self, key):
|
|
||||||
dict.__delitem__(self, key)
|
|
||||||
|
|
||||||
def __getattr__(self, key):
|
|
||||||
try:
|
|
||||||
return self.__getitem__(self._get_key(key))
|
|
||||||
except KeyError:
|
|
||||||
raise AttributeError(key)
|
|
||||||
|
|
||||||
def __setattr__(self, key, value):
|
|
||||||
self.__setitem__(self._get_key(key), value)
|
|
||||||
|
|
||||||
def __delattr__(self, key):
|
|
||||||
try:
|
|
||||||
object.__delattr__(self, key)
|
|
||||||
except AttributeError:
|
|
||||||
self.__delitem__(self._get_key(key))
|
|
||||||
|
|
||||||
def _plain_dict(self):
|
|
||||||
d = {k: v for (k, v) in self.items() if k[0] != "_"}
|
|
||||||
return d
|
|
||||||
|
|
||||||
|
|
||||||
def register(rsubclass, rtype=None):
|
|
||||||
_subtypes[rtype or rsubclass.__name__] = rsubclass
|
|
||||||
|
|
||||||
|
|
||||||
_subtypes = {}
|
|
||||||
|
|
||||||
|
|
||||||
def from_dict(indict, cls=None):
|
def from_dict(indict, cls=None):
|
||||||
if not cls:
|
if not cls:
|
||||||
target = indict.get('@type', None)
|
target = indict.get('@type', None)
|
||||||
|
cls = BaseModel
|
||||||
try:
|
try:
|
||||||
if target and target in _subtypes:
|
cls = subtypes()[target]
|
||||||
cls = _subtypes[target]
|
except KeyError:
|
||||||
else:
|
pass
|
||||||
cls = BaseModel
|
|
||||||
except Exception:
|
|
||||||
cls = BaseModel
|
|
||||||
outdict = dict()
|
outdict = dict()
|
||||||
for k, v in indict.items():
|
for k, v in indict.items():
|
||||||
if k == '@context':
|
if k == '@context':
|
||||||
@@ -270,10 +249,11 @@ def from_dict(indict, cls=None):
|
|||||||
elif isinstance(v, dict):
|
elif isinstance(v, dict):
|
||||||
v = from_dict(indict[k])
|
v = from_dict(indict[k])
|
||||||
elif isinstance(v, list):
|
elif isinstance(v, list):
|
||||||
|
v = v[:]
|
||||||
for ix, v2 in enumerate(v):
|
for ix, v2 in enumerate(v):
|
||||||
if isinstance(v2, dict):
|
if isinstance(v2, dict):
|
||||||
v[ix] = from_dict(v2)
|
v[ix] = from_dict(v2)
|
||||||
outdict[k] = v
|
outdict[k] = copy.copy(v)
|
||||||
return cls(**outdict)
|
return cls(**outdict)
|
||||||
|
|
||||||
|
|
||||||
@@ -286,37 +266,54 @@ def from_json(injson):
|
|||||||
return from_dict(indict)
|
return from_dict(indict)
|
||||||
|
|
||||||
|
|
||||||
def from_schema(name, schema=None, schema_file=None, base_classes=None):
|
class Entry(BaseModel):
|
||||||
|
schema = 'entry'
|
||||||
|
|
||||||
|
text = alias('nif:isString')
|
||||||
|
|
||||||
|
|
||||||
|
class Sentiment(BaseModel):
|
||||||
|
schema = 'sentiment'
|
||||||
|
|
||||||
|
polarity = alias('marl:hasPolarity')
|
||||||
|
polarityValue = alias('marl:hasPolarityValue')
|
||||||
|
|
||||||
|
|
||||||
|
class Error(BaseModel, Exception):
|
||||||
|
schema = 'error'
|
||||||
|
|
||||||
|
def __init__(self, message='Generic senpy exception', *args, **kwargs):
|
||||||
|
Exception.__init__(self, message)
|
||||||
|
super(Error, self).__init__(*args, **kwargs)
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if not hasattr(self, 'errors'):
|
||||||
|
return self.message
|
||||||
|
return '{}:\n\t{}'.format(self.message, self.errors)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return Exception.__hash__(self)
|
||||||
|
|
||||||
|
|
||||||
|
# Add the remaining schemas programmatically
|
||||||
|
|
||||||
|
def _class_from_schema(name, schema=None, schema_file=None, base_classes=None):
|
||||||
base_classes = base_classes or []
|
base_classes = base_classes or []
|
||||||
base_classes.append(BaseModel)
|
base_classes.append(BaseModel)
|
||||||
schema_file = schema_file or '{}.json'.format(name)
|
attrs = {}
|
||||||
class_name = '{}{}'.format(name[0].upper(), name[1:])
|
if schema:
|
||||||
if '/' not in 'schema_file':
|
attrs['schema'] = schema
|
||||||
schema_file = os.path.join(os.path.dirname(os.path.realpath(__file__)),
|
elif schema_file:
|
||||||
'schemas',
|
attrs['schema_file'] = schema_file
|
||||||
schema_file)
|
else:
|
||||||
|
attrs['schema'] = name
|
||||||
schema_path = 'file://' + schema_file
|
name = "".join((name[0].upper(), name[1:]))
|
||||||
|
return BaseMeta(name, base_classes, attrs)
|
||||||
with open(schema_file) as f:
|
|
||||||
schema = json.load(f)
|
|
||||||
|
|
||||||
dct = {}
|
|
||||||
|
|
||||||
resolver = jsonschema.RefResolver(schema_path, schema)
|
|
||||||
dct['@type'] = name
|
|
||||||
dct['_schema_file'] = schema_file
|
|
||||||
dct['schema'] = schema
|
|
||||||
dct['_validator'] = jsonschema.Draft4Validator(schema, resolver=resolver)
|
|
||||||
|
|
||||||
newclass = type(class_name, tuple(base_classes), dct)
|
|
||||||
|
|
||||||
register(newclass, name)
|
|
||||||
return newclass
|
|
||||||
|
|
||||||
|
|
||||||
def _add_from_schema(*args, **kwargs):
|
def _add_class_from_schema(*args, **kwargs):
|
||||||
generatedClass = from_schema(*args, **kwargs)
|
generatedClass = _class_from_schema(*args, **kwargs)
|
||||||
globals()[generatedClass.__name__] = generatedClass
|
globals()[generatedClass.__name__] = generatedClass
|
||||||
del generatedClass
|
del generatedClass
|
||||||
|
|
||||||
@@ -330,55 +327,19 @@ for i in [
|
|||||||
'emotionModel',
|
'emotionModel',
|
||||||
'emotionPlugin',
|
'emotionPlugin',
|
||||||
'emotionSet',
|
'emotionSet',
|
||||||
'entry',
|
'entity',
|
||||||
'help',
|
'help',
|
||||||
'plugin',
|
'plugin',
|
||||||
'plugins',
|
'plugins',
|
||||||
'response',
|
'response',
|
||||||
'results',
|
'results',
|
||||||
'sentiment',
|
|
||||||
'sentimentPlugin',
|
'sentimentPlugin',
|
||||||
'suggestion',
|
'suggestion',
|
||||||
|
'aggregatedEvaluation',
|
||||||
|
'evaluation',
|
||||||
|
'metric',
|
||||||
|
'dataset',
|
||||||
|
'datasets',
|
||||||
|
|
||||||
]:
|
]:
|
||||||
_add_from_schema(i)
|
_add_class_from_schema(i)
|
||||||
|
|
||||||
_ErrorModel = from_schema('error')
|
|
||||||
|
|
||||||
|
|
||||||
class Error(SenpyMixin, Exception):
|
|
||||||
def __init__(self, message, *args, **kwargs):
|
|
||||||
super(Error, self).__init__(self, message, message)
|
|
||||||
self._error = _ErrorModel(message=message, *args, **kwargs)
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
def validate(self, obj=None):
|
|
||||||
self._error.validate()
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
return self._error[key]
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
self._error[key] = value
|
|
||||||
|
|
||||||
def __delitem__(self, key):
|
|
||||||
del self._error[key]
|
|
||||||
|
|
||||||
def __getattr__(self, key):
|
|
||||||
if key != '_error' and hasattr(self._error, key):
|
|
||||||
return getattr(self._error, key)
|
|
||||||
raise AttributeError(key)
|
|
||||||
|
|
||||||
def __setattr__(self, key, value):
|
|
||||||
if key != '_error':
|
|
||||||
return setattr(self._error, key, value)
|
|
||||||
else:
|
|
||||||
super(Error, self).__setattr__(key, value)
|
|
||||||
|
|
||||||
def __delattr__(self, key):
|
|
||||||
delattr(self._error, key)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self.to_JSON(with_context=False))
|
|
||||||
|
|
||||||
|
|
||||||
register(Error, 'error')
|
|
||||||
|
@@ -1,14 +1,17 @@
|
|||||||
from future import standard_library
|
from future import standard_library
|
||||||
standard_library.install_aliases()
|
standard_library.install_aliases()
|
||||||
|
|
||||||
|
|
||||||
|
from future.utils import with_metaclass
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import pickle
|
import pickle
|
||||||
import logging
|
import logging
|
||||||
import tempfile
|
|
||||||
import copy
|
import copy
|
||||||
|
import pprint
|
||||||
|
|
||||||
import fnmatch
|
|
||||||
import inspect
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -16,26 +19,92 @@ import importlib
|
|||||||
import yaml
|
import yaml
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
from .. import models, utils
|
from .. import models, utils
|
||||||
from ..api import API_PARAMS
|
from .. import api
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from gsitk.evaluation.evaluation import Evaluation as Eval
|
||||||
|
from sklearn.pipeline import Pipeline
|
||||||
|
GSITK_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
logger.warn('GSITK is not installed. Some functions will be unavailable.')
|
||||||
|
GSITK_AVAILABLE = False
|
||||||
|
|
||||||
class Plugin(models.Plugin):
|
|
||||||
def __init__(self, info=None):
|
class PluginMeta(models.BaseMeta):
|
||||||
|
_classes = {}
|
||||||
|
|
||||||
|
def __new__(mcs, name, bases, attrs, **kwargs):
|
||||||
|
plugin_type = []
|
||||||
|
if hasattr(bases[0], 'plugin_type'):
|
||||||
|
plugin_type += bases[0].plugin_type
|
||||||
|
plugin_type.append(name)
|
||||||
|
alias = attrs.get('name', name)
|
||||||
|
attrs['plugin_type'] = plugin_type
|
||||||
|
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))
|
||||||
|
attrs['description'] = doc
|
||||||
|
attrs['name'] = alias
|
||||||
|
cls = super(PluginMeta, mcs).__new__(mcs, name, bases, attrs)
|
||||||
|
|
||||||
|
if alias in mcs._classes:
|
||||||
|
if os.environ.get('SENPY_TESTING', ""):
|
||||||
|
raise Exception(('The type of plugin {} already exists. '
|
||||||
|
'Please, choose a different name').format(name))
|
||||||
|
else:
|
||||||
|
logger.warn('Overloading plugin class: {}'.format(alias))
|
||||||
|
mcs._classes[alias] = cls
|
||||||
|
return cls
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def for_type(cls, ptype):
|
||||||
|
return cls._classes[ptype]
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
||||||
|
'''
|
||||||
|
Base class for all plugins in senpy.
|
||||||
|
A plugin must provide at least these attributes:
|
||||||
|
|
||||||
|
- version
|
||||||
|
- description (or docstring)
|
||||||
|
- author
|
||||||
|
|
||||||
|
Additionally, they may provide a URL (url) of a repository or website.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, info=None, data_folder=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Provides a canonical name for plugins and serves as base for other
|
Provides a canonical name for plugins and serves as base for other
|
||||||
kinds of plugins.
|
kinds of plugins.
|
||||||
"""
|
"""
|
||||||
if not info:
|
|
||||||
raise models.Error(message=("You need to provide configuration"
|
|
||||||
"information for the plugin."))
|
|
||||||
logger.debug("Initialising {}".format(info))
|
logger.debug("Initialising {}".format(info))
|
||||||
id = 'plugins/{}_{}'.format(info['name'], info['version'])
|
super(Plugin, self).__init__(**kwargs)
|
||||||
super(Plugin, self).__init__(id=id, **info)
|
if info:
|
||||||
|
self.update(info)
|
||||||
|
self.validate()
|
||||||
|
self.id = 'plugins/{}_{}'.format(self['name'], self['version'])
|
||||||
self.is_activated = False
|
self.is_activated = False
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
self.data_folder = data_folder or os.getcwd()
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
missing = []
|
||||||
|
for x in ['name', 'description', 'version']:
|
||||||
|
if x not in self:
|
||||||
|
missing.append(x)
|
||||||
|
if missing:
|
||||||
|
raise models.Error('Missing configuration parameters: {}'.format(missing))
|
||||||
|
|
||||||
def get_folder(self):
|
def get_folder(self):
|
||||||
return os.path.dirname(inspect.getfile(self.__class__))
|
return os.path.dirname(inspect.getfile(self.__class__))
|
||||||
@@ -46,30 +115,62 @@ class Plugin(models.Plugin):
|
|||||||
def deactivate(self):
|
def deactivate(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test(self):
|
def test(self, test_cases=None):
|
||||||
|
if not test_cases:
|
||||||
if not hasattr(self, 'test_cases'):
|
if not hasattr(self, 'test_cases'):
|
||||||
import inspect
|
|
||||||
raise AttributeError(('Plugin {} [{}] does not have any defined '
|
raise AttributeError(('Plugin {} [{}] does not have any defined '
|
||||||
'test cases').format(self.id, inspect.getfile(self.__class__)))
|
'test cases').format(self.id,
|
||||||
for case in self.test_cases:
|
inspect.getfile(self.__class__)))
|
||||||
res = list(self.analyse_entry(models.Entry(case['entry']),
|
test_cases = self.test_cases
|
||||||
case['params']))
|
for case in test_cases:
|
||||||
exp = case['expected']
|
try:
|
||||||
if not isinstance(exp, list):
|
self.test_case(case)
|
||||||
exp = [exp]
|
logger.debug('Test case passed:\n{}'.format(pprint.pformat(case)))
|
||||||
utils.check_template(res, exp)
|
except Exception as ex:
|
||||||
|
logger.warn('Test case failed:\n{}'.format(pprint.pformat(case)))
|
||||||
|
raise
|
||||||
|
|
||||||
|
def test_case(self, case):
|
||||||
|
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)
|
||||||
|
try:
|
||||||
|
params = api.parse_params(given_parameters, self.extra_params)
|
||||||
|
res = list(self.analyse_entries([entry, ], params))
|
||||||
|
|
||||||
|
if not isinstance(expected, list):
|
||||||
|
expected = [expected]
|
||||||
|
utils.check_template(res, expected)
|
||||||
for r in res:
|
for r in res:
|
||||||
r.validate()
|
r.validate()
|
||||||
|
except models.Error:
|
||||||
|
if should_fail:
|
||||||
|
return
|
||||||
|
raise
|
||||||
|
assert not should_fail
|
||||||
|
|
||||||
|
def open(self, fpath, *args, **kwargs):
|
||||||
|
if not os.path.isabs(fpath):
|
||||||
|
fpath = os.path.join(self.data_folder, fpath)
|
||||||
|
return open(fpath, *args, **kwargs)
|
||||||
|
|
||||||
|
def serve(self, debug=True, **kwargs):
|
||||||
|
utils.easy(plugin_list=[self, ], plugin_folder=None, debug=debug, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# For backwards compatibility
|
||||||
SenpyPlugin = Plugin
|
SenpyPlugin = Plugin
|
||||||
|
|
||||||
|
|
||||||
class AnalysisPlugin(Plugin):
|
class Analysis(Plugin):
|
||||||
|
'''
|
||||||
|
A subclass of Plugin that analyses text and provides an annotation.
|
||||||
|
'''
|
||||||
|
|
||||||
def analyse(self, *args, **kwargs):
|
def analyse(self, *args, **kwargs):
|
||||||
raise NotImplemented(
|
raise NotImplementedError(
|
||||||
'Your method should implement either analyse or analyse_entry')
|
'Your plugin should implement either analyse or analyse_entry')
|
||||||
|
|
||||||
def analyse_entry(self, entry, parameters):
|
def analyse_entry(self, entry, parameters):
|
||||||
""" An implemented plugin should override this method.
|
""" An implemented plugin should override this method.
|
||||||
@@ -88,40 +189,193 @@ class AnalysisPlugin(Plugin):
|
|||||||
def analyse_entries(self, entries, parameters):
|
def analyse_entries(self, entries, parameters):
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
logger.debug('Analysing entry with plugin {}: {}'.format(self, entry))
|
logger.debug('Analysing entry with plugin {}: {}'.format(self, entry))
|
||||||
for result in self.analyse_entry(entry, parameters):
|
results = self.analyse_entry(entry, parameters)
|
||||||
|
if inspect.isgenerator(results):
|
||||||
|
for result in results:
|
||||||
yield result
|
yield result
|
||||||
|
else:
|
||||||
|
yield results
|
||||||
|
|
||||||
|
def test_case(self, case):
|
||||||
|
if 'entry' not in case and 'input' in case:
|
||||||
|
entry = models.Entry(_auto_id=False)
|
||||||
|
entry.nif__isString = case['input']
|
||||||
|
case['entry'] = entry
|
||||||
|
super(Analysis, self).test_case(case)
|
||||||
|
|
||||||
|
|
||||||
class ConversionPlugin(Plugin):
|
AnalysisPlugin = Analysis
|
||||||
|
|
||||||
|
|
||||||
|
class Conversion(Plugin):
|
||||||
|
'''
|
||||||
|
A subclass of Plugins that convert between different annotation models.
|
||||||
|
e.g. a conversion of emotion models, or normalization of sentiment values.
|
||||||
|
'''
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SentimentPlugin(models.SentimentPlugin, AnalysisPlugin):
|
ConversionPlugin = Conversion
|
||||||
def __init__(self, info, *args, **kwargs):
|
|
||||||
super(SentimentPlugin, self).__init__(info, *args, **kwargs)
|
|
||||||
self.minPolarityValue = float(info.get("minPolarityValue", 0))
|
|
||||||
self.maxPolarityValue = float(info.get("maxPolarityValue", 1))
|
|
||||||
|
|
||||||
|
|
||||||
class EmotionPlugin(models.EmotionPlugin, AnalysisPlugin):
|
class SentimentPlugin(Analysis, models.SentimentPlugin):
|
||||||
def __init__(self, info, *args, **kwargs):
|
'''
|
||||||
super(EmotionPlugin, self).__init__(info, *args, **kwargs)
|
Sentiment plugins provide sentiment annotation (using Marl)
|
||||||
self.minEmotionValue = float(info.get("minEmotionValue", -1))
|
'''
|
||||||
self.maxEmotionValue = float(info.get("maxEmotionValue", 1))
|
minPolarityValue = 0
|
||||||
|
maxPolarityValue = 1
|
||||||
|
|
||||||
|
def test_case(self, case):
|
||||||
|
if 'polarity' in case:
|
||||||
|
expected = case.get('expected', {})
|
||||||
|
s = models.Sentiment(_auto_id=False)
|
||||||
|
s.marl__hasPolarity = case['polarity']
|
||||||
|
if 'sentiments' not in expected:
|
||||||
|
expected['sentiments'] = []
|
||||||
|
expected['sentiments'].append(s)
|
||||||
|
case['expected'] = expected
|
||||||
|
super(SentimentPlugin, self).test_case(case)
|
||||||
|
|
||||||
|
|
||||||
class EmotionConversionPlugin(models.EmotionConversionPlugin, ConversionPlugin):
|
class EmotionPlugin(Analysis, models.EmotionPlugin):
|
||||||
|
'''
|
||||||
|
Emotion plugins provide emotion annotation (using Onyx)
|
||||||
|
'''
|
||||||
|
minEmotionValue = 0
|
||||||
|
maxEmotionValue = 1
|
||||||
|
|
||||||
|
|
||||||
|
class EmotionConversion(Conversion):
|
||||||
|
'''
|
||||||
|
A subclass of Conversion that converts emotion annotations using different models
|
||||||
|
'''
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
EmotionConversionPlugin = EmotionConversion
|
||||||
|
|
||||||
|
|
||||||
|
class Box(AnalysisPlugin):
|
||||||
|
'''
|
||||||
|
Black box plugins delegate analysis to a function.
|
||||||
|
The flow is like so:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
entry --> input() --> predict_one() --> output() --> entry'
|
||||||
|
|
||||||
|
|
||||||
|
In other words: their ``input`` method convers a query (entry and a set of parameters) into
|
||||||
|
the input to the box method. The ``output`` method convers the results given by the box into
|
||||||
|
an entry that senpy can handle.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def input(self, entry, params=None):
|
||||||
|
'''Transforms a query (entry+param) into an input for the black box'''
|
||||||
|
return entry
|
||||||
|
|
||||||
|
def output(self, output, entry=None, params=None):
|
||||||
|
'''Transforms the results of the black box into an entry'''
|
||||||
|
return output
|
||||||
|
|
||||||
|
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.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 np.array([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 = Pipeline([('plugin', self)])
|
||||||
|
pipe.name = self.name
|
||||||
|
return pipe
|
||||||
|
|
||||||
|
|
||||||
|
class TextBox(Box):
|
||||||
|
'''A black box plugin that takes only text as input'''
|
||||||
|
|
||||||
|
def input(self, entry, params):
|
||||||
|
entry = super(TextBox, self).input(entry, params)
|
||||||
|
return entry['nif:isString']
|
||||||
|
|
||||||
|
|
||||||
|
class SentimentBox(TextBox, SentimentPlugin):
|
||||||
|
'''
|
||||||
|
A box plugin where the output is only a polarity label or a tuple (polarity, polarityValue)
|
||||||
|
'''
|
||||||
|
|
||||||
|
def output(self, output, entry, **kwargs):
|
||||||
|
s = models.Sentiment()
|
||||||
|
try:
|
||||||
|
label, value = output
|
||||||
|
except ValueError:
|
||||||
|
label, value = output, None
|
||||||
|
s.prov(self)
|
||||||
|
s.polarity = label
|
||||||
|
if value is not None:
|
||||||
|
s.polarityValue = value
|
||||||
|
entry.sentiments.append(s)
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
class EmotionBox(TextBox, EmotionPlugin):
|
||||||
|
'''
|
||||||
|
A box plugin where the output is only an a tuple of emotion labels
|
||||||
|
'''
|
||||||
|
|
||||||
|
def output(self, output, entry, **kwargs):
|
||||||
|
if not isinstance(output, list):
|
||||||
|
output = [output]
|
||||||
|
s = models.EmotionSet()
|
||||||
|
entry.emotions.append(s)
|
||||||
|
for label in output:
|
||||||
|
e = models.Emotion(onyx__hasEmotionCategory=label)
|
||||||
|
s.append(e)
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
class MappingMixin(object):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mappings(self):
|
||||||
|
return self._mappings
|
||||||
|
|
||||||
|
@mappings.setter
|
||||||
|
def mappings(self, value):
|
||||||
|
self._mappings = value
|
||||||
|
|
||||||
|
def output(self, output, entry, params):
|
||||||
|
output = self.mappings.get(output,
|
||||||
|
self.mappings.get('default', output))
|
||||||
|
return super(MappingMixin, self).output(output=output,
|
||||||
|
entry=entry,
|
||||||
|
params=params)
|
||||||
|
|
||||||
|
|
||||||
class ShelfMixin(object):
|
class ShelfMixin(object):
|
||||||
@property
|
@property
|
||||||
def sh(self):
|
def sh(self):
|
||||||
if not hasattr(self, '_sh') or self._sh is None:
|
if not hasattr(self, '_sh') or self._sh is None:
|
||||||
self.__dict__['_sh'] = {}
|
self._sh = {}
|
||||||
if os.path.isfile(self.shelf_file):
|
if os.path.isfile(self.shelf_file):
|
||||||
try:
|
try:
|
||||||
self.__dict__['_sh'] = pickle.load(open(self.shelf_file, 'rb'))
|
with self.open(self.shelf_file, 'rb') as p:
|
||||||
|
self._sh = pickle.load(p)
|
||||||
except (IndexError, EOFError, pickle.UnpicklingError):
|
except (IndexError, EOFError, pickle.UnpicklingError):
|
||||||
logger.warning('{} has a corrupted shelf file!'.format(self.id))
|
logger.warning('{} has a corrupted shelf file!'.format(self.id))
|
||||||
if not self.get('force_shelf', False):
|
if not self.get('force_shelf', False):
|
||||||
@@ -132,36 +386,42 @@ class ShelfMixin(object):
|
|||||||
def sh(self):
|
def sh(self):
|
||||||
if os.path.isfile(self.shelf_file):
|
if os.path.isfile(self.shelf_file):
|
||||||
os.remove(self.shelf_file)
|
os.remove(self.shelf_file)
|
||||||
del self.__dict__['_sh']
|
del self._sh
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
@sh.setter
|
||||||
|
def sh(self, value):
|
||||||
|
self._sh = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def shelf_file(self):
|
def shelf_file(self):
|
||||||
if 'shelf_file' not in self or not self['shelf_file']:
|
if not hasattr(self, '_shelf_file') or not self._shelf_file:
|
||||||
sd = os.environ.get('SENPY_DATA', tempfile.gettempdir())
|
self._shelf_file = os.path.join(self.data_folder, self.name + '.p')
|
||||||
self.shelf_file = os.path.join(sd, self.name + '.p')
|
return self._shelf_file
|
||||||
return self['shelf_file']
|
|
||||||
|
@shelf_file.setter
|
||||||
|
def shelf_file(self, value):
|
||||||
|
self._shelf_file = value
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
logger.debug('saving pickle')
|
logger.debug('saving pickle')
|
||||||
if hasattr(self, '_sh') and self._sh is not None:
|
if hasattr(self, '_sh') and self._sh is not None:
|
||||||
with open(self.shelf_file, 'wb') as f:
|
with self.open(self.shelf_file, 'wb') as f:
|
||||||
pickle.dump(self._sh, f)
|
pickle.dump(self._sh, f)
|
||||||
|
|
||||||
|
|
||||||
default_plugin_type = API_PARAMS['plugin_type']['default']
|
|
||||||
|
|
||||||
|
|
||||||
def pfilter(plugins, **kwargs):
|
def pfilter(plugins, **kwargs):
|
||||||
""" Filter plugins by different criteria """
|
""" Filter plugins by different criteria """
|
||||||
if isinstance(plugins, models.Plugins):
|
if isinstance(plugins, models.Plugins):
|
||||||
plugins = plugins.plugins
|
plugins = plugins.plugins
|
||||||
elif isinstance(plugins, dict):
|
elif isinstance(plugins, dict):
|
||||||
plugins = plugins.values()
|
plugins = plugins.values()
|
||||||
ptype = kwargs.pop('plugin_type', default_plugin_type)
|
ptype = kwargs.pop('plugin_type', Plugin)
|
||||||
logger.debug('#' * 100)
|
logger.debug('#' * 100)
|
||||||
logger.debug('ptype {}'.format(ptype))
|
logger.debug('ptype {}'.format(ptype))
|
||||||
if ptype:
|
if ptype:
|
||||||
|
if isinstance(ptype, PluginMeta):
|
||||||
|
ptype = ptype.__name__
|
||||||
try:
|
try:
|
||||||
ptype = ptype[0].upper() + ptype[1:]
|
ptype = ptype[0].upper() + ptype[1:]
|
||||||
pclass = globals()[ptype]
|
pclass = globals()[ptype]
|
||||||
@@ -183,11 +443,7 @@ def pfilter(plugins, **kwargs):
|
|||||||
|
|
||||||
if kwargs:
|
if kwargs:
|
||||||
candidates = filter(matches, candidates)
|
candidates = filter(matches, candidates)
|
||||||
return {p.name: p for p in candidates}
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
def validate_info(info):
|
|
||||||
return all(x in info for x in ('name', 'module', 'description', 'version'))
|
|
||||||
|
|
||||||
|
|
||||||
def load_module(name, root=None):
|
def load_module(name, root=None):
|
||||||
@@ -199,7 +455,7 @@ def load_module(name, root=None):
|
|||||||
return tmp
|
return tmp
|
||||||
|
|
||||||
|
|
||||||
def log_subprocess_output(process):
|
def _log_subprocess_output(process):
|
||||||
for line in iter(process.stdout.readline, b''):
|
for line in iter(process.stdout.readline, b''):
|
||||||
logger.info('%r', line)
|
logger.info('%r', line)
|
||||||
for line in iter(process.stderr.readline, b''):
|
for line in iter(process.stderr.readline, b''):
|
||||||
@@ -207,77 +463,176 @@ def log_subprocess_output(process):
|
|||||||
|
|
||||||
|
|
||||||
def install_deps(*plugins):
|
def install_deps(*plugins):
|
||||||
|
installed = False
|
||||||
for info in plugins:
|
for info in plugins:
|
||||||
requirements = info.get('requirements', [])
|
requirements = info.get('requirements', [])
|
||||||
if requirements:
|
if requirements:
|
||||||
pip_args = ['pip']
|
pip_args = [sys.executable, '-m', 'pip', 'install']
|
||||||
pip_args.append('install')
|
|
||||||
pip_args.append('--use-wheel')
|
|
||||||
for req in requirements:
|
for req in requirements:
|
||||||
pip_args.append(req)
|
pip_args.append(req)
|
||||||
logger.info('Installing requirements: ' + str(requirements))
|
logger.info('Installing requirements: ' + str(requirements))
|
||||||
process = subprocess.Popen(pip_args,
|
process = subprocess.Popen(pip_args,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE)
|
stderr=subprocess.PIPE)
|
||||||
log_subprocess_output(process)
|
_log_subprocess_output(process)
|
||||||
exitcode = process.wait()
|
exitcode = process.wait()
|
||||||
|
installed = True
|
||||||
if exitcode != 0:
|
if exitcode != 0:
|
||||||
raise models.Error("Dependencies not properly installed")
|
raise models.Error("Dependencies not properly installed")
|
||||||
|
return installed
|
||||||
|
|
||||||
|
|
||||||
def load_plugin_from_info(info, root=None, validator=validate_info, install=True):
|
is_plugin_file = re.compile(r'.*\.senpy$|senpy_[a-zA-Z0-9_]+\.py$|'
|
||||||
if not root and '_path' in info:
|
'^(?!test_)[a-zA-Z0-9_]+_plugin.py$')
|
||||||
root = os.path.dirname(info['_path'])
|
|
||||||
if not validator(info):
|
|
||||||
raise ValueError('Plugin info is not valid: {}'.format(info))
|
|
||||||
module = info["module"]
|
|
||||||
|
|
||||||
try:
|
|
||||||
tmp = load_module(module, root)
|
|
||||||
except ImportError:
|
|
||||||
if not install:
|
|
||||||
raise
|
|
||||||
install_deps(info)
|
|
||||||
tmp = load_module(module, root)
|
|
||||||
candidate = None
|
|
||||||
for _, obj in inspect.getmembers(tmp):
|
|
||||||
if inspect.isclass(obj) and inspect.getmodule(obj) == tmp:
|
|
||||||
logger.debug(("Found plugin class:"
|
|
||||||
" {}@{}").format(obj, inspect.getmodule(obj)))
|
|
||||||
candidate = obj
|
|
||||||
break
|
|
||||||
if not candidate:
|
|
||||||
logger.debug("No valid plugin for: {}".format(module))
|
|
||||||
return
|
|
||||||
module = candidate(info=info)
|
|
||||||
return module
|
|
||||||
|
|
||||||
|
|
||||||
def parse_plugin_info(fpath):
|
def find_plugins(folders):
|
||||||
logger.debug("Loading plugin: {}".format(fpath))
|
|
||||||
with open(fpath, 'r') as f:
|
|
||||||
info = yaml.load(f)
|
|
||||||
info['_path'] = fpath
|
|
||||||
name = info['name']
|
|
||||||
return name, info
|
|
||||||
|
|
||||||
|
|
||||||
def load_plugin(fpath):
|
|
||||||
name, info = parse_plugin_info(fpath)
|
|
||||||
logger.debug("Info: {}".format(info))
|
|
||||||
plugin = load_plugin_from_info(info)
|
|
||||||
return name, plugin
|
|
||||||
|
|
||||||
|
|
||||||
def load_plugins(folders, loader=load_plugin):
|
|
||||||
plugins = {}
|
|
||||||
for search_folder in folders:
|
for search_folder in folders:
|
||||||
for root, dirnames, filenames in os.walk(search_folder):
|
for root, dirnames, filenames in os.walk(search_folder):
|
||||||
# Do not look for plugins in hidden or special folders
|
# Do not look for plugins in hidden or special folders
|
||||||
dirnames[:] = [d for d in dirnames if d[0] not in ['.', '_']]
|
dirnames[:] = [d for d in dirnames if d[0] not in ['.', '_']]
|
||||||
for filename in fnmatch.filter(filenames, '*.senpy'):
|
for filename in filter(is_plugin_file.match, filenames):
|
||||||
fpath = os.path.join(root, filename)
|
fpath = os.path.join(root, filename)
|
||||||
name, plugin = loader(fpath)
|
yield fpath
|
||||||
if plugin and name:
|
|
||||||
plugins[name] = plugin
|
|
||||||
|
def from_path(fpath, **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
|
||||||
|
root = os.path.dirname(fpath)
|
||||||
|
module = os.path.basename(fpath)[:-3]
|
||||||
|
for instance in _from_module_name(module=module, root=root, **kwargs):
|
||||||
|
yield instance
|
||||||
|
else:
|
||||||
|
info = parse_plugin_info(fpath)
|
||||||
|
yield from_info(info, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def from_folder(folders, loader=from_path, **kwargs):
|
||||||
|
plugins = []
|
||||||
|
for fpath in find_plugins(folders):
|
||||||
|
for plugin in loader(fpath, **kwargs):
|
||||||
|
plugins.append(plugin)
|
||||||
return plugins
|
return plugins
|
||||||
|
|
||||||
|
|
||||||
|
def from_info(info, root=None, **kwargs):
|
||||||
|
if any(x not in info for x in ('module',)):
|
||||||
|
raise ValueError('Plugin info is not valid: {}'.format(info))
|
||||||
|
module = info["module"]
|
||||||
|
|
||||||
|
if not root and '_path' in info:
|
||||||
|
root = os.path.dirname(info['_path'])
|
||||||
|
|
||||||
|
return one_from_module(module, root=root, info=info, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_plugin_info(fpath):
|
||||||
|
logger.debug("Parsing plugin info: {}".format(fpath))
|
||||||
|
with open(fpath, 'r') as f:
|
||||||
|
info = yaml.load(f)
|
||||||
|
info['_path'] = fpath
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def from_module(module, **kwargs):
|
||||||
|
|
||||||
|
if inspect.ismodule(module):
|
||||||
|
res = _from_loaded_module(module, **kwargs)
|
||||||
|
else:
|
||||||
|
res = _from_module_name(module, **kwargs)
|
||||||
|
for p in res:
|
||||||
|
yield p
|
||||||
|
|
||||||
|
|
||||||
|
def one_from_module(module, root, info, **kwargs):
|
||||||
|
if '@type' in info:
|
||||||
|
cls = PluginMeta.from_type(info['@type'])
|
||||||
|
return cls(info=info, **kwargs)
|
||||||
|
instance = next(from_module(module=module, root=root, info=info, **kwargs), None)
|
||||||
|
if not instance:
|
||||||
|
raise Exception("No valid plugin for: {}".format(module))
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
def _classes_in_module(module):
|
||||||
|
for _, obj in inspect.getmembers(module):
|
||||||
|
if inspect.isclass(obj) and inspect.getmodule(obj) == module:
|
||||||
|
logger.debug(("Found plugin class:"
|
||||||
|
" {}@{}").format(obj, inspect.getmodule(obj)))
|
||||||
|
yield obj
|
||||||
|
|
||||||
|
|
||||||
|
def _instances_in_module(module):
|
||||||
|
for _, obj in inspect.getmembers(module):
|
||||||
|
if isinstance(obj, Plugin) and inspect.getmodule(obj) == module:
|
||||||
|
logger.debug(("Found plugin instance:"
|
||||||
|
" {}@{}").format(obj, inspect.getmodule(obj)))
|
||||||
|
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)
|
||||||
|
module = load_module(module, root)
|
||||||
|
for plugin in _from_loaded_module(module=module, root=root, info=info, **kwargs):
|
||||||
|
yield plugin
|
||||||
|
|
||||||
|
|
||||||
|
def _from_loaded_module(module, info=None, **kwargs):
|
||||||
|
for cls in _classes_in_module(module):
|
||||||
|
yield cls(info=info, **kwargs)
|
||||||
|
for instance in _instances_in_module(module):
|
||||||
|
yield instance
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate(plugins, datasets, **kwargs):
|
||||||
|
if not GSITK_AVAILABLE:
|
||||||
|
raise Exception('GSITK is not available. Install it to use this function.')
|
||||||
|
|
||||||
|
ev = Eval(tuples=None,
|
||||||
|
datasets=datasets,
|
||||||
|
pipelines=[plugin.as_pipe() for plugin in plugins])
|
||||||
|
ev.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
|
||||||
|
@@ -6,7 +6,12 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class CentroidConversion(EmotionConversionPlugin):
|
class CentroidConversion(EmotionConversionPlugin):
|
||||||
def __init__(self, info):
|
'''
|
||||||
|
This plugin converts emotion annotations from a dimensional model to a
|
||||||
|
categorical one, and vice versa. The centroids used in the conversion
|
||||||
|
are configurable and appear in the semantic description of the plugin.
|
||||||
|
'''
|
||||||
|
def __init__(self, info, *args, **kwargs):
|
||||||
if 'centroids' not in info:
|
if 'centroids' not in info:
|
||||||
raise Error('Centroid conversion plugins should provide '
|
raise Error('Centroid conversion plugins should provide '
|
||||||
'the centroids in their senpy file')
|
'the centroids in their senpy file')
|
||||||
@@ -33,7 +38,7 @@ class CentroidConversion(EmotionConversionPlugin):
|
|||||||
ncentroids[aliases.get(k1, k1)] = nv1
|
ncentroids[aliases.get(k1, k1)] = nv1
|
||||||
info['centroids'] = ncentroids
|
info['centroids'] = ncentroids
|
||||||
|
|
||||||
super(CentroidConversion, self).__init__(info)
|
super(CentroidConversion, self).__init__(info, *args, **kwargs)
|
||||||
|
|
||||||
self.dimensions = set()
|
self.dimensions = set()
|
||||||
for c in self.centroids.values():
|
for c in self.centroids.values():
|
||||||
|
@@ -4,7 +4,15 @@ from senpy.plugins import EmotionPlugin
|
|||||||
from senpy.models import EmotionSet, Emotion, Entry
|
from senpy.models import EmotionSet, Emotion, Entry
|
||||||
|
|
||||||
|
|
||||||
class RmoRandPlugin(EmotionPlugin):
|
class EmoRand(EmotionPlugin):
|
||||||
|
name = "emoRand"
|
||||||
|
description = 'A sample plugin that returns a random emotion annotation'
|
||||||
|
author = '@balkian'
|
||||||
|
version = '0.1'
|
||||||
|
url = "https://github.com/gsi-upm/senpy-plugins-community"
|
||||||
|
requirements = {}
|
||||||
|
onyx__usesEmotionModel = "emoml:big6"
|
||||||
|
|
||||||
def analyse_entry(self, entry, params):
|
def analyse_entry(self, entry, params):
|
||||||
category = "emoml:big6happiness"
|
category = "emoml:big6happiness"
|
||||||
number = max(-1, min(1, random.gauss(0, 0.5)))
|
number = max(-1, min(1, random.gauss(0, 0.5)))
|
||||||
|
@@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
name: emoRand
|
|
||||||
module: emoRand
|
|
||||||
description: A sample plugin that returns a random emotion annotation
|
|
||||||
author: "@balkian"
|
|
||||||
version: '0.1'
|
|
||||||
url: "https://github.com/gsi-upm/senpy-plugins-community"
|
|
||||||
requirements: {}
|
|
||||||
onyx:usesEmotionModel: "emoml:big6"
|
|
32
senpy/plugins/example/emorand_plugin.py
Normal file
32
senpy/plugins/example/emorand_plugin.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import random
|
||||||
|
|
||||||
|
from senpy.plugins import EmotionPlugin
|
||||||
|
from senpy.models import EmotionSet, Emotion, Entry
|
||||||
|
|
||||||
|
|
||||||
|
class EmoRand(EmotionPlugin):
|
||||||
|
'''A sample plugin that returns a random emotion annotation'''
|
||||||
|
author = '@balkian'
|
||||||
|
version = '0.1'
|
||||||
|
url = "https://github.com/gsi-upm/senpy-plugins-community"
|
||||||
|
onyx__usesEmotionModel = "emoml:big6"
|
||||||
|
|
||||||
|
def analyse_entry(self, entry, params):
|
||||||
|
category = "emoml:big6happiness"
|
||||||
|
number = max(-1, min(1, random.gauss(0, 0.5)))
|
||||||
|
if number > 0:
|
||||||
|
category = "emoml:big6anger"
|
||||||
|
emotionSet = EmotionSet()
|
||||||
|
emotion = Emotion({"onyx:hasEmotionCategory": category})
|
||||||
|
emotionSet.onyx__hasEmotion.append(emotion)
|
||||||
|
emotionSet.prov__wasGeneratedBy = self.id
|
||||||
|
entry.emotions.append(emotionSet)
|
||||||
|
yield entry
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
params = dict()
|
||||||
|
results = list()
|
||||||
|
for i in range(100):
|
||||||
|
res = next(self.analyse_entry(Entry(nif__isString="Hello"), params))
|
||||||
|
res.validate()
|
||||||
|
results.append(res.emotions[0]['onyx:hasEmotion'][0]['onyx:hasEmotionCategory'])
|
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
name: rand
|
|
||||||
module: rand
|
|
||||||
description: A sample plugin that returns a random sentiment annotation
|
|
||||||
author: "@balkian"
|
|
||||||
version: '0.1'
|
|
||||||
url: "https://github.com/gsi-upm/senpy-plugins-community"
|
|
||||||
requirements: {}
|
|
||||||
marl:maxPolarityValue: '1'
|
|
||||||
marl:minPolarityValue: "-1"
|
|
@@ -1,33 +1,35 @@
|
|||||||
import random
|
import random
|
||||||
|
from senpy import SentimentPlugin, Sentiment, Entry
|
||||||
from senpy.plugins import SentimentPlugin
|
|
||||||
from senpy.models import Sentiment, Entry
|
|
||||||
|
|
||||||
|
|
||||||
class RandPlugin(SentimentPlugin):
|
class Rand(SentimentPlugin):
|
||||||
|
'''A sample plugin that returns a random sentiment annotation'''
|
||||||
|
author = "@balkian"
|
||||||
|
version = '0.1'
|
||||||
|
url = "https://github.com/gsi-upm/senpy-plugins-community"
|
||||||
|
marl__maxPolarityValue = '1'
|
||||||
|
marl__minPolarityValue = "-1"
|
||||||
|
|
||||||
def analyse_entry(self, entry, params):
|
def analyse_entry(self, entry, params):
|
||||||
lang = params.get("language", "auto")
|
|
||||||
|
|
||||||
polarity_value = max(-1, min(1, random.gauss(0.2, 0.2)))
|
polarity_value = max(-1, min(1, random.gauss(0.2, 0.2)))
|
||||||
polarity = "marl:Neutral"
|
polarity = "marl:Neutral"
|
||||||
if polarity_value > 0:
|
if polarity_value > 0:
|
||||||
polarity = "marl:Positive"
|
polarity = "marl:Positive"
|
||||||
elif polarity_value < 0:
|
elif polarity_value < 0:
|
||||||
polarity = "marl:Negative"
|
polarity = "marl:Negative"
|
||||||
sentiment = Sentiment({
|
sentiment = Sentiment(marl__hasPolarity=polarity,
|
||||||
"marl:hasPolarity": polarity,
|
marl__polarityValue=polarity_value)
|
||||||
"marl:polarityValue": polarity_value
|
sentiment.prov(self)
|
||||||
})
|
|
||||||
sentiment["prov:wasGeneratedBy"] = self.id
|
|
||||||
entry.sentiments.append(sentiment)
|
entry.sentiments.append(sentiment)
|
||||||
entry.language = lang
|
|
||||||
yield entry
|
yield entry
|
||||||
|
|
||||||
def test(self):
|
def test(self):
|
||||||
|
'''Run several random analyses.'''
|
||||||
params = dict()
|
params = dict()
|
||||||
results = list()
|
results = list()
|
||||||
for i in range(100):
|
for i in range(50):
|
||||||
res = next(self.analyse_entry(Entry(nif__isString="Hello"), params))
|
res = next(self.analyse_entry(Entry(nif__isString="Hello"),
|
||||||
|
params))
|
||||||
res.validate()
|
res.validate()
|
||||||
results.append(res.sentiments[0]['marl:hasPolarity'])
|
results.append(res.sentiments[0]['marl:hasPolarity'])
|
||||||
assert 'marl:Positive' in results
|
assert 'marl:Positive' in results
|
@@ -6,19 +6,21 @@ import nltk
|
|||||||
|
|
||||||
|
|
||||||
class SplitPlugin(AnalysisPlugin):
|
class SplitPlugin(AnalysisPlugin):
|
||||||
|
'''description: A sample plugin that chunks input text'''
|
||||||
|
|
||||||
def activate(self):
|
def activate(self):
|
||||||
nltk.download('punkt')
|
nltk.download('punkt')
|
||||||
|
|
||||||
def analyse_entry(self, entry, params):
|
def analyse_entry(self, entry, params):
|
||||||
chunker_type = params.get("delimiter", "sentence")
|
chunker_type = params["delimiter"]
|
||||||
original_text = entry.get('nif:isString', None)
|
original_text = entry['nif:isString']
|
||||||
if chunker_type == "sentence":
|
if chunker_type == "sentence":
|
||||||
tokenizer = PunktSentenceTokenizer()
|
tokenizer = PunktSentenceTokenizer()
|
||||||
if chunker_type == "paragraph":
|
if chunker_type == "paragraph":
|
||||||
tokenizer = LineTokenizer()
|
tokenizer = LineTokenizer()
|
||||||
chars = tokenizer.span_tokenize(original_text)
|
chars = list(tokenizer.span_tokenize(original_text))
|
||||||
for i, chunk in enumerate(tokenizer.tokenize(original_text)):
|
for i, chunk in enumerate(tokenizer.tokenize(original_text)):
|
||||||
|
print(chunk)
|
||||||
e = Entry()
|
e = Entry()
|
||||||
e['nif:isString'] = chunk
|
e['nif:isString'] = chunk
|
||||||
if entry.id:
|
if entry.id:
|
||||||
@@ -44,20 +46,20 @@ class SplitPlugin(AnalysisPlugin):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'entry': {
|
'entry': {
|
||||||
"id": ":test",
|
"@id": ":test",
|
||||||
'nif:isString': 'Hello. World.'
|
'nif:isString': 'Hello\nWorld'
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'delimiter': 'sentence',
|
'delimiter': 'paragraph',
|
||||||
},
|
},
|
||||||
'expected': [
|
'expected': [
|
||||||
{
|
{
|
||||||
"@id": ":test#char=0,6",
|
"@id": ":test#char=0,5",
|
||||||
'nif:isString': 'Hello.'
|
'nif:isString': 'Hello'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@id": ":test#char=7,13",
|
"@id": ":test#char=6,11",
|
||||||
'nif:isString': 'World.'
|
'nif:isString': 'World'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -6,8 +6,9 @@ from senpy.models import Sentiment
|
|||||||
|
|
||||||
|
|
||||||
class Sentiment140Plugin(SentimentPlugin):
|
class Sentiment140Plugin(SentimentPlugin):
|
||||||
|
'''Connects to the sentiment140 free API: http://sentiment140.com'''
|
||||||
def analyse_entry(self, entry, params):
|
def analyse_entry(self, entry, params):
|
||||||
lang = params.get("language", "auto")
|
lang = params["language"]
|
||||||
res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson",
|
res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson",
|
||||||
json.dumps({
|
json.dumps({
|
||||||
"language": lang,
|
"language": lang,
|
||||||
@@ -35,6 +36,18 @@ class Sentiment140Plugin(SentimentPlugin):
|
|||||||
entry.language = lang
|
entry.language = lang
|
||||||
yield entry
|
yield entry
|
||||||
|
|
||||||
|
def test(self, *args, **kwargs):
|
||||||
|
'''
|
||||||
|
To avoid calling the sentiment140 API, we will mock the results
|
||||||
|
from requests.
|
||||||
|
'''
|
||||||
|
from senpy.test import patch_requests
|
||||||
|
expected = {"data": [{"polarity": 4}]}
|
||||||
|
with patch_requests(expected) as (request, response):
|
||||||
|
super(Sentiment140Plugin, self).test(*args, **kwargs)
|
||||||
|
assert request.called
|
||||||
|
assert response.json.called
|
||||||
|
|
||||||
test_cases = [
|
test_cases = [
|
||||||
{
|
{
|
||||||
'entry': {
|
'entry': {
|
||||||
|
@@ -16,6 +16,7 @@ extra_params:
|
|||||||
- es
|
- es
|
||||||
- en
|
- en
|
||||||
- auto
|
- auto
|
||||||
|
default: auto
|
||||||
requirements: {}
|
requirements: {}
|
||||||
maxPolarityValue: 1
|
maxPolarityValue: 1
|
||||||
minPolarityValue: 0
|
minPolarityValue: 0
|
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"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
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": {
|
"Response": {
|
||||||
"$ref": "response.json"
|
"$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": "Evalation",
|
||||||
|
"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"]
|
||||||
|
}
|
@@ -7,11 +7,11 @@
|
|||||||
"description": "Help containing accepted parameters",
|
"description": "Help containing accepted parameters",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"parameters": {
|
"valid_parameters": {
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": "parameters"
|
"required": "valid_parameters"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
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"]
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["@id", "extra_params"],
|
"required": ["@id", "name", "description", "version", "plugin_type"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"@id": {
|
"@id": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -9,7 +9,19 @@
|
|||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The name of the plugin, which will be used in the algorithm detection phase"
|
"description": "The name of the plugin, which will be used in the algorithm detection phase."
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A summary of what the plugin does, and pointers to further information."
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The version of the plugin."
|
||||||
|
},
|
||||||
|
"plugin_type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Sub-type of plugin. e.g. sentimentPlugin"
|
||||||
},
|
},
|
||||||
"extra_params": {
|
"extra_params": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@@ -152,3 +152,18 @@ textarea{
|
|||||||
/* background: white; */
|
/* background: white; */
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.deco-none {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deco-none:link {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deco-none:hover {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
var ONYX = "http://www.gsi.dit.upm.es/ontologies/onyx/ns#";
|
var ONYX = "http://www.gsi.dit.upm.es/ontologies/onyx/ns#";
|
||||||
var RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
|
var RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
|
||||||
var plugins_params={};
|
var plugins_params = default_params = {};
|
||||||
var default_params = JSON.parse($.ajax({type: "GET", url: "/api?help=True" , async: false}).responseText);
|
var plugins = [];
|
||||||
|
var defaultPlugin = {};
|
||||||
|
var gplugins = {};
|
||||||
|
|
||||||
function replaceURLWithHTMLLinks(text) {
|
function replaceURLWithHTMLLinks(text) {
|
||||||
console.log('Text: ' + text);
|
console.log('Text: ' + text);
|
||||||
var exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
|
var exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
|
||||||
@@ -25,59 +28,71 @@ function hashchanged(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
var response = JSON.parse($.ajax({type: "GET", url: "/api/plugins/" , async: false}).responseText);
|
function get_plugins(response){
|
||||||
var defaultPlugin= JSON.parse($.ajax({type: "GET", url: "/api/plugins/default" , async: false}).responseText);
|
|
||||||
html="";
|
|
||||||
var availablePlugins = document.getElementById('availablePlugins');
|
|
||||||
plugins = response.plugins;
|
plugins = response.plugins;
|
||||||
gplugins = {};
|
}
|
||||||
|
|
||||||
|
function get_datasets(response){
|
||||||
|
datasets = response.datasets
|
||||||
|
}
|
||||||
|
|
||||||
|
function group_plugins(){
|
||||||
for (r in plugins){
|
for (r in plugins){
|
||||||
ptype = plugins[r]['@type'];
|
ptype = plugins[r]['@type'];
|
||||||
if(gplugins[ptype] == undefined){
|
if(gplugins[ptype] == undefined){
|
||||||
gplugins[ptype] = [r]
|
gplugins[ptype] = [r];
|
||||||
}else{
|
}else{
|
||||||
gplugins[ptype].push(r)
|
gplugins[ptype].push(r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_parameters(){
|
||||||
|
for (p in plugins){
|
||||||
|
plugin = plugins[p];
|
||||||
|
if (plugin["extra_params"]){
|
||||||
|
plugins_params[plugin["name"]] = plugin["extra_params"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw_plugins_selection(){
|
||||||
|
html="";
|
||||||
|
group_plugins();
|
||||||
for (g in gplugins){
|
for (g in gplugins){
|
||||||
html += "<optgroup label=\""+g+"\">"
|
html += "<optgroup label=\""+g+"\">"
|
||||||
for (r in gplugins[g]){
|
for (r in gplugins[g]){
|
||||||
plugin = plugins[r]
|
plugin = plugins[gplugins[g][r]]
|
||||||
if (plugin["name"]){
|
if (!plugin["name"]){
|
||||||
|
console.log("No name for plugin ", plugin);
|
||||||
|
continue;
|
||||||
|
|
||||||
|
}
|
||||||
|
html+= "<option value=\""+plugin.name+"\" "
|
||||||
if (plugin["name"] == defaultPlugin["name"]){
|
if (plugin["name"] == defaultPlugin["name"]){
|
||||||
if (plugin["is_activated"]){
|
html+= " selected=\"selected\""
|
||||||
html+= "<option value=\""+plugin["name"]+"\" selected=\"selected\">"+plugin["name"]+"</option>"
|
|
||||||
}else{
|
|
||||||
html+= "<option value=\""+plugin["name"]+"\" selected=\"selected\" disabled=\"disabled\">"+plugin["name"]+"</option>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
if (plugin["is_activated"]){
|
|
||||||
html+= "<option value=\""+plugin["name"]+"\">"+plugin["name"]+"</option>"
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
html+= "<option value=\""+plugin["name"]+"\" disabled=\"disabled\">"+plugin["name"]+"</option>"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (!plugin["is_activated"]){
|
||||||
|
html+= " disabled=\"disabled\" "
|
||||||
}
|
}
|
||||||
|
html+=">"+plugin["name"]+"</option>"
|
||||||
|
|
||||||
if (plugin["extra_params"]){
|
|
||||||
plugins_params[plugin["name"]]={};
|
|
||||||
for (param in plugin["extra_params"]){
|
|
||||||
if (typeof plugin["extra_params"][param] !="string"){
|
|
||||||
var params = new Array();
|
|
||||||
var alias = plugin["extra_params"][param]["aliases"][0];
|
|
||||||
params[alias]=new Array();
|
|
||||||
for (option in plugin["extra_params"][param]["options"]){
|
|
||||||
params[alias].push(plugin["extra_params"][param]["options"][option])
|
|
||||||
}
|
|
||||||
plugins_params[plugin["name"]][alias] = (params[alias])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
html += "</optgroup>"
|
||||||
var pluginList = document.createElement('li');
|
// Two elements with plugin class
|
||||||
|
// One from the evaluate tab and another one from the analyse tab
|
||||||
|
document.getElementsByClassName('plugin')[0].innerHTML = html;
|
||||||
|
document.getElementsByClassName('plugin')[1].innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw_plugins_list(){
|
||||||
|
var availablePlugins = document.getElementById('availablePlugins');
|
||||||
|
|
||||||
|
for(p in plugins){
|
||||||
|
var pluginEntry = document.createElement('li');
|
||||||
|
plugin = plugins[p];
|
||||||
newHtml = ""
|
newHtml = ""
|
||||||
if(plugin.url) {
|
if(plugin.url) {
|
||||||
newHtml= "<a href="+plugin.url+">" + plugin.name + "</a>";
|
newHtml= "<a href="+plugin.url+">" + plugin.name + "</a>";
|
||||||
@@ -85,13 +100,34 @@ $(document).ready(function() {
|
|||||||
newHtml= plugin["name"];
|
newHtml= plugin["name"];
|
||||||
}
|
}
|
||||||
newHtml += ": " + replaceURLWithHTMLLinks(plugin.description);
|
newHtml += ": " + replaceURLWithHTMLLinks(plugin.description);
|
||||||
pluginList.innerHTML = newHtml;
|
pluginEntry.innerHTML = newHtml;
|
||||||
availablePlugins.appendChild(pluginList)
|
availablePlugins.appendChild(pluginEntry)
|
||||||
}
|
}
|
||||||
html += "</optgroup>"
|
}
|
||||||
|
|
||||||
|
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('plugins').innerHTML = html;
|
document.getElementById("datasets").innerHTML = html;
|
||||||
change_params();
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
var response = JSON.parse($.ajax({type: "GET", url: "/api/plugins/" , async: false}).responseText);
|
||||||
|
defaultPlugin= JSON.parse($.ajax({type: "GET", url: "/api/plugins/default" , async: false}).responseText);
|
||||||
|
var response2 = JSON.parse($.ajax({type: "GET", url: "/api/datasets/" , async: false}).responseText);
|
||||||
|
|
||||||
|
get_plugins(response);
|
||||||
|
get_default_parameters();
|
||||||
|
get_datasets(response2);
|
||||||
|
|
||||||
|
draw_plugins_list();
|
||||||
|
draw_plugins_selection();
|
||||||
|
draw_parameters();
|
||||||
|
draw_datasets();
|
||||||
|
|
||||||
$(window).on('hashchange', hashchanged);
|
$(window).on('hashchange', hashchanged);
|
||||||
hashchanged();
|
hashchanged();
|
||||||
@@ -99,38 +135,132 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function get_default_parameters(){
|
||||||
|
default_params = JSON.parse($.ajax({type: "GET", url: "/api?help=true" , async: false}).responseText).valid_parameters;
|
||||||
|
// Remove the parameters that are always added
|
||||||
|
delete default_params["input"];
|
||||||
|
delete default_params["algorithm"];
|
||||||
|
delete default_params["help"];
|
||||||
|
|
||||||
function change_params(){
|
}
|
||||||
var plugin = document.getElementById("plugins").options[document.getElementById("plugins").selectedIndex].value;
|
|
||||||
html=""
|
|
||||||
for (param in default_params){
|
|
||||||
if ((default_params[param]['options']) && (['help','conversion'].indexOf(param) < 0)){
|
|
||||||
html+= "<label> "+param+"</label>"
|
|
||||||
html+= "<select id=\""+param+"\" name=\""+param+"\">"
|
|
||||||
for (option in default_params[param]['options']){
|
|
||||||
if (default_params[param]['options'][option] == default_params[param]['default']){
|
|
||||||
html+="<option value \""+default_params[param]['options'][option]+"\" selected >"+default_params[param]['options'][option]+"</option>"
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
html+="<option value \""+default_params[param]['options'][option]+"\">"+default_params[param]['options'][option]+"</option>"
|
|
||||||
|
|
||||||
|
function draw_default_parameters(){
|
||||||
|
var basic_params = document.getElementById("basic_params");
|
||||||
|
basic_params.innerHTML = params_div(default_params);
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw_extra_parameters(){
|
||||||
|
var plugin = document.getElementsByClassName('plugin')[0].options[document.getElementsByClassName('plugin')[0].selectedIndex].value;
|
||||||
|
get_parameters();
|
||||||
|
|
||||||
|
var extra_params = document.getElementById("extra_params");
|
||||||
|
extra_params.innerHTML = params_div(plugins_params[plugin]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw_parameters(){
|
||||||
|
draw_default_parameters();
|
||||||
|
draw_extra_parameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function add_default_params(){
|
||||||
|
var html = "";
|
||||||
|
// html += '<a href="#basic_params" class="btn btn-info" data-toggle="collapse">Basic API parameters</a>';
|
||||||
|
html += '<span id="basic_params" class="panel-collapse collapse">';
|
||||||
|
html += '<ul class="list-group">'
|
||||||
|
html += params_div(default_params);
|
||||||
|
html += '</span>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function params_div(params){
|
||||||
|
var html = '<div class="container-fluid">';
|
||||||
|
if (Object.keys(params).length === 0) {
|
||||||
|
html += '<p class="text text-muted text-center">This plugin does not take any extra parameters</p>';
|
||||||
}
|
}
|
||||||
|
// Iterate over the keys in order
|
||||||
|
pnames = Object.keys(params).sort()
|
||||||
|
for (ix in pnames){
|
||||||
|
pname = pnames[ix];
|
||||||
|
param = params[pname];
|
||||||
|
html+='<div class="form-group">';
|
||||||
|
html += '<div class="row">'
|
||||||
|
html+= '<label class="col-sm-4" for="'+pname+'">'+pname+'</label>'
|
||||||
|
if (param.options){
|
||||||
|
opts = param.options;
|
||||||
|
if(param.options.length == 1 && param.options[0] == 'boolean') {
|
||||||
|
opts = [true, false];
|
||||||
}
|
}
|
||||||
html+="</select><br>"
|
html+= '<select class="col-sm-8" id="'+pname+"\" name=\""+pname+"\">"
|
||||||
}
|
var defaultopt = param.default;
|
||||||
}
|
for (option in opts){
|
||||||
for (param in plugins_params[plugin]){
|
isselected = "";
|
||||||
if (param || plugins_params[plugin][param].length > 1){
|
if (defaultopt != undefined && opts[option] == defaultopt ){
|
||||||
html+= "<label> Parameter "+param+"</label>"
|
isselected = ' selected="selected"'
|
||||||
html+= "<select id=\""+param+"\" name=\""+param+"\">"
|
|
||||||
for (option in plugins_params[plugin][param]){
|
|
||||||
html+="<option value \""+plugins_params[plugin][param][option]+"\">"+plugins_params[plugin][param][option]+"</option>"
|
|
||||||
}
|
}
|
||||||
|
html+="<option value=\""+opts[option]+'"' + isselected +
|
||||||
|
'>'+opts[option]+"</option>"
|
||||||
}
|
}
|
||||||
html+="</select>"
|
html+="</select>"
|
||||||
}
|
}
|
||||||
document.getElementById("params").innerHTML = html
|
else {
|
||||||
};
|
default_value = "";
|
||||||
|
if(param.default != undefined){
|
||||||
|
default_value = param.default;
|
||||||
|
};
|
||||||
|
html +='<input class="col-sm-8" id="'+pname+'" name="'+pname+'" value="' + default_value + '"></input>';
|
||||||
|
}
|
||||||
|
html+='</div>';
|
||||||
|
html+='<div class="row">';
|
||||||
|
if ('description' in param){
|
||||||
|
html += '<p class="form-text sm-sm-12 text-muted text-center">' + param.description + '</p>';
|
||||||
|
|
||||||
|
}
|
||||||
|
html+='</div>';
|
||||||
|
html+='</div>';
|
||||||
|
}
|
||||||
|
html+='</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _get_form_parameters(id){
|
||||||
|
var element = document.getElementById(id);
|
||||||
|
params = {};
|
||||||
|
var selects = element.getElementsByTagName('select');
|
||||||
|
var inputs = element.getElementsByTagName('input');
|
||||||
|
|
||||||
|
Array.prototype.forEach.call(selects, function (sel) {
|
||||||
|
key = sel.name;
|
||||||
|
value = sel.options[sel.selectedIndex].value
|
||||||
|
params[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
Array.prototype.forEach.call(inputs, function (el) {
|
||||||
|
params[el.name] = el.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (k in params){
|
||||||
|
value = params[k];
|
||||||
|
if (value == "" || value === "undefined"){
|
||||||
|
delete params[k];
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_form_parameters(){
|
||||||
|
var p1 = _get_form_parameters("basic_params");
|
||||||
|
var p2 = _get_form_parameters("extra_params");
|
||||||
|
return Object.assign(p1, p2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function add_param(key, value){
|
||||||
|
value = encodeURIComponent(value);
|
||||||
|
return "&"+key+"="+value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function load_JSON(){
|
function load_JSON(){
|
||||||
url = "/api";
|
url = "/api";
|
||||||
@@ -138,29 +268,20 @@ function load_JSON(){
|
|||||||
var rawcontainer = document.getElementById("jsonraw");
|
var rawcontainer = document.getElementById("jsonraw");
|
||||||
rawcontainer.innerHTML = '';
|
rawcontainer.innerHTML = '';
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
var plugin = document.getElementById("plugins").options[document.getElementById("plugins").selectedIndex].value;
|
|
||||||
|
var plugin = document.getElementsByClassName("plugin")[0].options[document.getElementsByClassName("plugin")[0].selectedIndex].value;
|
||||||
|
|
||||||
var input = encodeURIComponent(document.getElementById("input").value);
|
var input = encodeURIComponent(document.getElementById("input").value);
|
||||||
url += "?algo="+plugin+"&i="+input
|
url += "?algo="+plugin+"&i="+input
|
||||||
for (param in plugins_params[plugin]){
|
|
||||||
if (param != null){
|
|
||||||
var param_value = encodeURIComponent(document.getElementById(param).options[document.getElementById(param).selectedIndex].text);
|
|
||||||
if (param_value){
|
|
||||||
url+="&"+param+"="+param_value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (param in default_params){
|
params = get_form_parameters();
|
||||||
if ((param != null) && (default_params[param]['options']) && (['help','conversion'].indexOf(param) < 0)){
|
|
||||||
var param_value = encodeURIComponent(document.getElementById(param).options[document.getElementById(param).selectedIndex].text);
|
for (key in params){
|
||||||
if (param_value){
|
url += add_param(key, params[key]);
|
||||||
url+="&"+param+"="+param_value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = $.ajax({type: "GET", url: url , async: false}).responseText;
|
var response = $.ajax({type: "GET", url: url , async: false}).responseText;
|
||||||
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response)
|
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
|
||||||
|
|
||||||
document.getElementById("input_request").innerHTML = "<a href='"+url+"'>"+url+"</a>"
|
document.getElementById("input_request").innerHTML = "<a href='"+url+"'>"+url+"</a>"
|
||||||
document.getElementById("results-div").style.display = 'block';
|
document.getElementById("results-div").style.display = 'block';
|
||||||
@@ -171,12 +292,95 @@ function load_JSON(){
|
|||||||
};
|
};
|
||||||
var editor = new JSONEditor(container, options, response);
|
var editor = new JSONEditor(container, options, response);
|
||||||
editor.expandAll();
|
editor.expandAll();
|
||||||
|
// $('#results-div a[href="#viewer"]').tab('show');
|
||||||
|
$('#results-div a[href="#viewer"]').click();
|
||||||
|
// location.hash = 'raw';
|
||||||
}
|
}
|
||||||
catch(err){
|
catch(err){
|
||||||
console.log("Error decoding JSON (got turtle?)");
|
console.log("Error decoding JSON (got turtle?)");
|
||||||
|
$('#results-div a[href="#raw"]').click();
|
||||||
|
// location.hash = 'raw';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
var response = $.ajax({type: "GET", url: url , async: false, dataType: 'json'}).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?)");
|
||||||
|
$('#evaluate-div a[href="#evaluate-raw"]').click();
|
||||||
|
// location.hash = 'raw';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@@ -32,6 +32,8 @@
|
|||||||
<ul class="nav nav-tabs" role="tablist">
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
<li role="presentation" ><a class="active" href="#about">About</a></li>
|
<li role="presentation" ><a class="active" href="#about">About</a></li>
|
||||||
<li role="presentation"class="active"><a class="active" href="#test">Test it</a></li>
|
<li role="presentation"class="active"><a class="active" href="#test">Test it</a></li>
|
||||||
|
<li role="presentation"><a class="active" href="#evaluate">Evaluate Plugins</a></li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
@@ -54,6 +56,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>List all available plugins: <a href="/api/plugins">/api/plugins</a></li>
|
<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>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>
|
<li>Download the JSON-LD context used: <a href="/api/contexts/Results.jsonld">/api/contexts/Results.jsonld</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -88,28 +91,52 @@
|
|||||||
|
|
||||||
<div class="tab-pane active" id="test">
|
<div class="tab-pane active" id="test">
|
||||||
<div class="well">
|
<div class="well">
|
||||||
<form id="form" onsubmit="return getPlugins();" accept-charset="utf-8">
|
<form id="form" class="container" onsubmit="return getPlugins();" accept-charset="utf-8">
|
||||||
<div id="inputswrapper">
|
|
||||||
<div><textarea id="input" class="boxsizingBorder" rows="5" name="i">This text makes me sad.
|
<div><textarea id="input" class="boxsizingBorder" rows="5" name="i">This text makes me sad.
|
||||||
whilst this text makes me happy and surprised at the same time.
|
whilst this text makes me happy and surprised at the same time.
|
||||||
I cannot believe it!</textarea></div>
|
I cannot believe it!</textarea>
|
||||||
<label>Select the plugin:</label>
|
|
||||||
<select id="plugins" name="plugins" onchange="change_params()">
|
|
||||||
</select>
|
|
||||||
</br>
|
|
||||||
<div id ="params">
|
|
||||||
</div>
|
</div>
|
||||||
</br>
|
<div>
|
||||||
|
<label>Select the plugin:</label>
|
||||||
|
<select id="plugins" name="plugins" class=plugin onchange="draw_extra_parameters()">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<!-- PARAMETERS -->
|
||||||
|
<div class="panel-group" id="parameters">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<a data-toggle="collapse" class="deco-none" href="#basic_params">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
Basic API parameters
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div id="basic_params" class="panel-collapse collapse panel-body">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<a data-toggle="collapse" class="deco-none" href="#extra_params">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
Plugin extra parameters
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div id="extra_params" class="panel-collapse collapse in panel-body">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- END PARAMETERS -->
|
||||||
|
|
||||||
<a id="preview" class="btn btn-lg btn-primary" onclick="load_JSON()">Analyse!</a>
|
<a id="preview" class="btn btn-lg btn-primary" onclick="load_JSON()">Analyse!</a>
|
||||||
<!--<button id="visualise" name="type" type="button">Visualise!</button>-->
|
<!--<button id="visualise" name="type" type="button">Visualise!</button>-->
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<span id="input_request"></span>
|
<span id="input_request"></span>
|
||||||
<div id="results-div">
|
<div id="results-div">
|
||||||
<ul class="nav nav-tabs" role="tablist">
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
<li role="presentation" class="active"><a class="active" href="#viewer">Viewer</a></li>
|
<li role="presentation" class="active"><a data-toggle="tab" class="active" href="#viewer">Viewer</a></li>
|
||||||
<li role="presentation"><a class="active" href="#raw">Raw</a></li>
|
<li role="presentation"><a data-toggle="tab" class="active" href="#raw">Raw</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content" id="results-container">
|
<div class="tab-content" id="results-container">
|
||||||
|
|
||||||
@@ -127,6 +154,70 @@ I cannot believe it!</textarea></div>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<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="preview" 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a href="http://www.gsi.dit.upm.es" target="_blank"><img class="center-block" src="static/img/gsi.png"/> </a>
|
<a href="http://www.gsi.dit.upm.es" target="_blank"><img class="center-block" src="static/img/gsi.png"/> </a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
36
senpy/test.py
Normal file
36
senpy/test.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
try:
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
except ImportError:
|
||||||
|
from mock import patch, MagicMock
|
||||||
|
|
||||||
|
from past.builtins import basestring
|
||||||
|
|
||||||
|
|
||||||
|
import json
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from .models import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def patch_requests(value, code=200):
|
||||||
|
success = MagicMock()
|
||||||
|
if isinstance(value, BaseModel):
|
||||||
|
value = value.jsonld()
|
||||||
|
if not isinstance(value, basestring):
|
||||||
|
data = json.dumps(value)
|
||||||
|
else:
|
||||||
|
data = value
|
||||||
|
|
||||||
|
success.json.return_value = value
|
||||||
|
|
||||||
|
success.status_code = code
|
||||||
|
success.content = data
|
||||||
|
success.text = data
|
||||||
|
|
||||||
|
method_mocker = MagicMock()
|
||||||
|
method_mocker.return_value = success
|
||||||
|
with patch.multiple('requests', request=method_mocker,
|
||||||
|
get=method_mocker, post=method_mocker):
|
||||||
|
yield method_mocker, success
|
||||||
|
assert method_mocker.called
|
@@ -1,25 +1,101 @@
|
|||||||
from . import models
|
from . import models, __version__
|
||||||
|
from collections import MutableMapping
|
||||||
|
import pprint
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# MutableMapping should be enough, but it causes problems with py2
|
||||||
|
DICTCLASSES = (MutableMapping, dict, models.BaseModel)
|
||||||
|
|
||||||
|
|
||||||
def check_template(indict, template):
|
def check_template(indict, template):
|
||||||
if isinstance(template, dict) and isinstance(indict, dict):
|
if isinstance(template, DICTCLASSES) and isinstance(indict, DICTCLASSES):
|
||||||
for k, v in template.items():
|
for k, v in template.items():
|
||||||
if k not in indict:
|
if k not in indict:
|
||||||
return '{} not in {}'.format(k, indict)
|
raise models.Error('{} not in {}'.format(k, indict))
|
||||||
check_template(indict[k], v)
|
check_template(indict[k], v)
|
||||||
elif isinstance(template, list) and isinstance(indict, list):
|
elif isinstance(template, list) and isinstance(indict, list):
|
||||||
if len(indict) != len(template):
|
|
||||||
raise models.Error('Different size for {} and {}'.format(indict, template))
|
|
||||||
for e in template:
|
for e in template:
|
||||||
found = False
|
|
||||||
for i in indict:
|
for i in indict:
|
||||||
try:
|
try:
|
||||||
check_template(i, e)
|
check_template(i, e)
|
||||||
found = True
|
break
|
||||||
except models.Error as ex:
|
except models.Error as ex:
|
||||||
|
# raise
|
||||||
continue
|
continue
|
||||||
if not found:
|
else:
|
||||||
raise models.Error('{} not found in {}'.format(e, indict))
|
raise models.Error(('Element not found.'
|
||||||
|
'\nExpected: {}\nIn: {}').format(pprint.pformat(e),
|
||||||
|
pprint.pformat(indict)))
|
||||||
else:
|
else:
|
||||||
if indict != template:
|
if indict != template:
|
||||||
raise models.Error('{} and {} are different'.format(indict, template))
|
raise models.Error(('Differences found.\n'
|
||||||
|
'\tExpected: {}\n'
|
||||||
|
'\tFound: {}').format(pprint.pformat(indict),
|
||||||
|
pprint.pformat(template)))
|
||||||
|
|
||||||
|
|
||||||
|
def convert_dictionary(original, mappings):
|
||||||
|
result = {}
|
||||||
|
for key, value in original.items():
|
||||||
|
if key in mappings:
|
||||||
|
key = mappings[key]
|
||||||
|
result[key] = value
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def easy_load(app=None, plugin_list=None, plugin_folder=None, **kwargs):
|
||||||
|
'''
|
||||||
|
Run a server with a specific plugin.
|
||||||
|
'''
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from .extensions import Senpy
|
||||||
|
|
||||||
|
if not app:
|
||||||
|
app = Flask(__name__)
|
||||||
|
sp = Senpy(app, plugin_folder=plugin_folder, **kwargs)
|
||||||
|
if not plugin_list:
|
||||||
|
from . import plugins
|
||||||
|
import __main__
|
||||||
|
plugin_list = plugins.from_module(__main__)
|
||||||
|
for plugin in plugin_list:
|
||||||
|
sp.add_plugin(plugin)
|
||||||
|
sp.install_deps()
|
||||||
|
sp.activate_all()
|
||||||
|
return sp, app
|
||||||
|
|
||||||
|
|
||||||
|
def easy_test(plugin_list=None):
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger().setLevel(logging.INFO)
|
||||||
|
if not plugin_list:
|
||||||
|
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!')
|
||||||
|
|
||||||
|
|
||||||
|
def easy(host='0.0.0.0', port=5000, debug=True, **kwargs):
|
||||||
|
'''
|
||||||
|
Run a server with a specific plugin.
|
||||||
|
'''
|
||||||
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger('senpy').setLevel(logging.INFO)
|
||||||
|
sp, app = easy_load(**kwargs)
|
||||||
|
easy_test(sp.plugins())
|
||||||
|
app.debug = debug
|
||||||
|
import time
|
||||||
|
logger.info(time.time())
|
||||||
|
logger.info('Senpy version {}'.format(__version__))
|
||||||
|
logger.info('Server running on port %s:%d. Ctrl+C to quit' % (host,
|
||||||
|
port))
|
||||||
|
app.debug = debug
|
||||||
|
app.run(host,
|
||||||
|
port,
|
||||||
|
debug=app.debug)
|
||||||
|
31
setup.py
31
setup.py
@@ -1,20 +1,20 @@
|
|||||||
import pip
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
# parse_requirements() returns generator of pip.req.InstallRequirement objects
|
|
||||||
from pip.req import parse_requirements
|
|
||||||
from senpy import __version__
|
|
||||||
|
|
||||||
try:
|
with open('senpy/VERSION') as f:
|
||||||
install_reqs = parse_requirements(
|
__version__ = f.read().strip()
|
||||||
"requirements.txt", session=pip.download.PipSession())
|
assert __version__
|
||||||
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")
|
|
||||||
|
|
||||||
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(
|
setup(
|
||||||
@@ -34,6 +34,9 @@ setup(
|
|||||||
install_requires=install_reqs,
|
install_requires=install_reqs,
|
||||||
tests_require=test_reqs,
|
tests_require=test_reqs,
|
||||||
setup_requires=['pytest-runner', ],
|
setup_requires=['pytest-runner', ],
|
||||||
|
extras_require={
|
||||||
|
'evaluation': extra_reqs
|
||||||
|
},
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts':
|
'console_scripts':
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
mock
|
mock
|
||||||
pytest-cov
|
pytest-cov
|
||||||
pytest
|
pytest
|
||||||
|
scikit-learn
|
||||||
|
numpy
|
||||||
|
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
name: Async
|
|
||||||
module: asyncplugin
|
|
||||||
description: I am async
|
|
||||||
author: "@balkian"
|
|
||||||
version: '0.1'
|
|
||||||
async: true
|
|
||||||
extra_params: {}
|
|
@@ -1,11 +0,0 @@
|
|||||||
from senpy.plugins import SentimentPlugin
|
|
||||||
|
|
||||||
|
|
||||||
class DummyPlugin(SentimentPlugin):
|
|
||||||
def analyse_entry(self, entry, params):
|
|
||||||
entry['nif:iString'] = entry['nif:isString'][::-1]
|
|
||||||
entry.reversed = entry.get('reversed', 0) + 1
|
|
||||||
yield entry
|
|
||||||
|
|
||||||
def test(self):
|
|
||||||
pass
|
|
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Dummy",
|
|
||||||
"module": "dummy",
|
|
||||||
"description": "I am dummy",
|
|
||||||
"author": "@balkian",
|
|
||||||
"version": "0.1",
|
|
||||||
"extra_params": {
|
|
||||||
"example": {
|
|
||||||
"@id": "example_parameter",
|
|
||||||
"aliases": ["example", "ex"],
|
|
||||||
"required": false,
|
|
||||||
"default": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "DummyRequired",
|
|
||||||
"module": "dummy",
|
|
||||||
"description": "I am dummy",
|
|
||||||
"author": "@balkian",
|
|
||||||
"version": "0.1",
|
|
||||||
"extra_params": {
|
|
||||||
"example": {
|
|
||||||
"@id": "example_parameter",
|
|
||||||
"aliases": ["example", "ex"],
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
from senpy.plugins import AnalysisPlugin
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
|
|
||||||
class SleepPlugin(AnalysisPlugin):
|
|
||||||
def activate(self, *args, **kwargs):
|
|
||||||
sleep(self.timeout)
|
|
||||||
|
|
||||||
def analyse_entry(self, entry, params):
|
|
||||||
sleep(float(params.get("timeout", self.timeout)))
|
|
||||||
yield entry
|
|
||||||
|
|
||||||
def test(self):
|
|
||||||
pass
|
|
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Sleep",
|
|
||||||
"module": "sleep",
|
|
||||||
"description": "I am dummy",
|
|
||||||
"author": "@balkian",
|
|
||||||
"version": "0.1",
|
|
||||||
"timeout": 0.05,
|
|
||||||
"extra_params": {
|
|
||||||
"timeout": {
|
|
||||||
"@id": "timeout_sleep",
|
|
||||||
"aliases": ["timeout", "to"],
|
|
||||||
"required": false,
|
|
||||||
"default": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -19,11 +19,8 @@ class APITest(TestCase):
|
|||||||
|
|
||||||
def test_basic(self):
|
def test_basic(self):
|
||||||
a = {}
|
a = {}
|
||||||
try:
|
self.assertRaises(Error, parse_params, a)
|
||||||
parse_params(a, NIF_PARAMS)
|
self.assertRaises(Error, parse_params, a, NIF_PARAMS)
|
||||||
raise AssertionError()
|
|
||||||
except Error:
|
|
||||||
pass
|
|
||||||
a = {'input': 'hello'}
|
a = {'input': 'hello'}
|
||||||
p = parse_params(a, NIF_PARAMS)
|
p = parse_params(a, NIF_PARAMS)
|
||||||
assert 'input' in p
|
assert 'input' in p
|
||||||
@@ -39,11 +36,7 @@ class APITest(TestCase):
|
|||||||
'required': True
|
'required': True
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try:
|
self.assertRaises(Error, parse_params, plug_params)
|
||||||
parse_params(query, plug_params)
|
|
||||||
raise AssertionError()
|
|
||||||
except Error:
|
|
||||||
pass
|
|
||||||
query['hello'] = 'world'
|
query['hello'] = 'world'
|
||||||
p = parse_params(query, plug_params)
|
p = parse_params(query, plug_params)
|
||||||
assert 'hello' in p
|
assert 'hello' in p
|
||||||
@@ -53,7 +46,6 @@ class APITest(TestCase):
|
|||||||
query['hiya'] = 'dlrow'
|
query['hiya'] = 'dlrow'
|
||||||
p = parse_params(query, plug_params)
|
p = parse_params(query, plug_params)
|
||||||
assert 'hello' in p
|
assert 'hello' in p
|
||||||
assert 'hiya' in p
|
|
||||||
assert p['hello'] == 'dlrow'
|
assert p['hello'] == 'dlrow'
|
||||||
|
|
||||||
def test_default(self):
|
def test_default(self):
|
||||||
|
@@ -23,7 +23,7 @@ class BlueprintsTest(TestCase):
|
|||||||
cls.app = Flask("test_extensions")
|
cls.app = Flask("test_extensions")
|
||||||
cls.app.debug = False
|
cls.app.debug = False
|
||||||
cls.client = cls.app.test_client()
|
cls.client = cls.app.test_client()
|
||||||
cls.senpy = Senpy()
|
cls.senpy = Senpy(default_plugins=True)
|
||||||
cls.senpy.init_app(cls.app)
|
cls.senpy.init_app(cls.app)
|
||||||
cls.dir = os.path.join(os.path.dirname(__file__), "..")
|
cls.dir = os.path.join(os.path.dirname(__file__), "..")
|
||||||
cls.senpy.add_folder(cls.dir)
|
cls.senpy.add_folder(cls.dir)
|
||||||
@@ -34,11 +34,14 @@ class BlueprintsTest(TestCase):
|
|||||||
def assertCode(self, resp, code):
|
def assertCode(self, resp, code):
|
||||||
self.assertEqual(resp.status_code, code)
|
self.assertEqual(resp.status_code, code)
|
||||||
|
|
||||||
|
def test_playground(self):
|
||||||
|
resp = self.client.get("/")
|
||||||
|
assert "main.js" in resp.data.decode()
|
||||||
|
|
||||||
def test_home(self):
|
def test_home(self):
|
||||||
"""
|
"""
|
||||||
Calling with no arguments should ask the user for more arguments
|
Calling with no arguments should ask the user for more arguments
|
||||||
"""
|
"""
|
||||||
self.app.debug = False
|
|
||||||
resp = self.client.get("/api/")
|
resp = self.client.get("/api/")
|
||||||
self.assertCode(resp, 400)
|
self.assertCode(resp, 400)
|
||||||
js = parse_resp(resp)
|
js = parse_resp(resp)
|
||||||
@@ -84,6 +87,10 @@ class BlueprintsTest(TestCase):
|
|||||||
js = parse_resp(resp)
|
js = parse_resp(resp)
|
||||||
logging.debug("Got response: %s", js)
|
logging.debug("Got response: %s", js)
|
||||||
assert isinstance(js, models.Error)
|
assert isinstance(js, models.Error)
|
||||||
|
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired&example=notvalid")
|
||||||
|
self.assertCode(resp, 400)
|
||||||
|
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired&example=a")
|
||||||
|
self.assertCode(resp, 200)
|
||||||
|
|
||||||
def test_error(self):
|
def test_error(self):
|
||||||
"""
|
"""
|
||||||
@@ -155,12 +162,15 @@ class BlueprintsTest(TestCase):
|
|||||||
def test_schema(self):
|
def test_schema(self):
|
||||||
resp = self.client.get("/api/schemas/definitions.json")
|
resp = self.client.get("/api/schemas/definitions.json")
|
||||||
self.assertCode(resp, 200)
|
self.assertCode(resp, 200)
|
||||||
js = parse_resp(resp)
|
assert "$schema" in resp.data.decode()
|
||||||
assert "$schema" in js
|
|
||||||
|
|
||||||
def test_help(self):
|
def test_help(self):
|
||||||
resp = self.client.get("/api/?help=true")
|
resp = self.client.get("/api/?help=true")
|
||||||
self.assertCode(resp, 200)
|
self.assertCode(resp, 200)
|
||||||
js = parse_resp(resp)
|
js = parse_resp(resp)
|
||||||
assert "parameters" in js
|
assert "valid_parameters" in js
|
||||||
assert "help" in js["parameters"]
|
assert "help" in js["valid_parameters"]
|
||||||
|
|
||||||
|
def test_conversion(self):
|
||||||
|
resp = self.client.get("/api/?input=hello&algo=emoRand&emotionModel=DOES NOT EXIST")
|
||||||
|
self.assertCode(resp, 404)
|
||||||
|
@@ -12,7 +12,8 @@ class CLITest(TestCase):
|
|||||||
def test_basic(self):
|
def test_basic(self):
|
||||||
self.assertRaises(Error, partial(main_function, []))
|
self.assertRaises(Error, partial(main_function, []))
|
||||||
|
|
||||||
res = main_function(['--input', 'test', '--algo', 'rand', '--with-parameters'])
|
res = main_function(['--input', 'test', '--algo', 'rand',
|
||||||
|
'--with-parameters', '--default-plugins'])
|
||||||
assert res.parameters['input'] == 'test'
|
assert res.parameters['input'] == 'test'
|
||||||
assert 'rand' in res.parameters['algorithm']
|
assert 'rand' in res.parameters['algorithm']
|
||||||
assert res.parameters['input'] == 'test'
|
assert res.parameters['input'] == 'test'
|
||||||
|
@@ -1,24 +1,9 @@
|
|||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
try:
|
|
||||||
from unittest.mock import patch
|
|
||||||
except ImportError:
|
|
||||||
from mock import patch
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
from senpy.test import patch_requests
|
||||||
from senpy.client import Client
|
from senpy.client import Client
|
||||||
from senpy.models import Results, Plugins, Error
|
from senpy.models import Results, Plugins, Error
|
||||||
from senpy.plugins import AnalysisPlugin, default_plugin_type
|
from senpy.plugins import AnalysisPlugin
|
||||||
|
|
||||||
|
|
||||||
class Call(dict):
|
|
||||||
def __init__(self, obj):
|
|
||||||
self.obj = obj.serialize()
|
|
||||||
self.status_code = 200
|
|
||||||
self.content = self.json()
|
|
||||||
|
|
||||||
def json(self):
|
|
||||||
return json.loads(self.obj)
|
|
||||||
|
|
||||||
|
|
||||||
class ModelsTest(TestCase):
|
class ModelsTest(TestCase):
|
||||||
@@ -29,20 +14,18 @@ class ModelsTest(TestCase):
|
|||||||
def test_client(self):
|
def test_client(self):
|
||||||
endpoint = 'http://dummy/'
|
endpoint = 'http://dummy/'
|
||||||
client = Client(endpoint)
|
client = Client(endpoint)
|
||||||
success = Call(Results())
|
with patch_requests(Results()) as (request, response):
|
||||||
with patch('requests.request', return_value=success) as patched:
|
|
||||||
resp = client.analyse('hello')
|
resp = client.analyse('hello')
|
||||||
assert isinstance(resp, Results)
|
assert isinstance(resp, Results)
|
||||||
patched.assert_called_with(
|
request.assert_called_with(
|
||||||
url=endpoint + '/', method='GET', params={'input': 'hello'})
|
url=endpoint + '/', method='GET', params={'input': 'hello'})
|
||||||
error = Call(Error('Nothing'))
|
with patch_requests(Error('Nothing')) as (request, response):
|
||||||
with patch('requests.request', return_value=error) as patched:
|
|
||||||
try:
|
try:
|
||||||
client.analyse(input='hello', algorithm='NONEXISTENT')
|
client.analyse(input='hello', algorithm='NONEXISTENT')
|
||||||
raise Exception('Exceptions should be raised. This is not golang')
|
raise Exception('Exceptions should be raised. This is not golang')
|
||||||
except Error:
|
except Error:
|
||||||
pass
|
pass
|
||||||
patched.assert_called_with(
|
request.assert_called_with(
|
||||||
url=endpoint + '/',
|
url=endpoint + '/',
|
||||||
method='GET',
|
method='GET',
|
||||||
params={'input': 'hello',
|
params={'input': 'hello',
|
||||||
@@ -54,12 +37,11 @@ class ModelsTest(TestCase):
|
|||||||
plugins = Plugins()
|
plugins = Plugins()
|
||||||
p1 = AnalysisPlugin({'name': 'AnalysisP1', 'version': 0, 'description': 'No'})
|
p1 = AnalysisPlugin({'name': 'AnalysisP1', 'version': 0, 'description': 'No'})
|
||||||
plugins.plugins = [p1, ]
|
plugins.plugins = [p1, ]
|
||||||
success = Call(plugins)
|
with patch_requests(plugins) as (request, response):
|
||||||
with patch('requests.request', return_value=success) as patched:
|
|
||||||
response = client.plugins()
|
response = client.plugins()
|
||||||
assert isinstance(response, dict)
|
assert isinstance(response, dict)
|
||||||
assert len(response) == 1
|
assert len(response) == 1
|
||||||
assert "AnalysisP1" in response
|
assert "AnalysisP1" in response
|
||||||
patched.assert_called_with(
|
request.assert_called_with(
|
||||||
url=endpoint + '/plugins', method='GET',
|
url=endpoint + '/plugins', method='GET',
|
||||||
params={'plugin_type': default_plugin_type})
|
params={})
|
||||||
|
@@ -25,10 +25,11 @@ def analyse(instance, **kwargs):
|
|||||||
class ExtensionsTest(TestCase):
|
class ExtensionsTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.app = Flask('test_extensions')
|
self.app = Flask('test_extensions')
|
||||||
self.dir = os.path.dirname(__file__)
|
self.examples_dir = os.path.join(os.path.dirname(__file__), '..', 'example-plugins')
|
||||||
self.senpy = Senpy(plugin_folder=self.dir,
|
self.senpy = Senpy(plugin_folder=self.examples_dir,
|
||||||
app=self.app,
|
app=self.app,
|
||||||
default_plugins=False)
|
default_plugins=False)
|
||||||
|
self.senpy.deactivate_all()
|
||||||
self.senpy.activate_plugin("Dummy", sync=True)
|
self.senpy.activate_plugin("Dummy", sync=True)
|
||||||
|
|
||||||
def test_init(self):
|
def test_init(self):
|
||||||
@@ -41,22 +42,37 @@ class ExtensionsTest(TestCase):
|
|||||||
def test_discovery(self):
|
def test_discovery(self):
|
||||||
""" Discovery of plugins in given folders. """
|
""" Discovery of plugins in given folders. """
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
assert self.dir in self.senpy._search_folders
|
print(self.senpy.plugins())
|
||||||
print(self.senpy.plugins)
|
assert self.senpy.get_plugin("dummy")
|
||||||
assert "Dummy" in self.senpy.plugins
|
|
||||||
|
def test_add_delete(self):
|
||||||
|
'''Should be able to add and delete new plugins. '''
|
||||||
|
new = plugins.Plugin(name='new', description='new', version=0)
|
||||||
|
self.senpy.add_plugin(new)
|
||||||
|
assert new in self.senpy.plugins()
|
||||||
|
self.senpy.delete_plugin(new)
|
||||||
|
assert new not in self.senpy.plugins()
|
||||||
|
|
||||||
|
def test_adding_folder(self):
|
||||||
|
""" It should be possible for senpy to look for plugins in more folders. """
|
||||||
|
senpy = Senpy(plugin_folder=None,
|
||||||
|
app=self.app,
|
||||||
|
default_plugins=False)
|
||||||
|
assert not senpy.analysis_plugins
|
||||||
|
senpy.add_folder(self.examples_dir)
|
||||||
|
assert senpy.analysis_plugins
|
||||||
|
self.assertRaises(AttributeError, senpy.add_folder, 'DOES NOT EXIST')
|
||||||
|
|
||||||
def test_installing(self):
|
def test_installing(self):
|
||||||
""" Installing a plugin """
|
""" Installing a plugin """
|
||||||
info = {
|
info = {
|
||||||
'name': 'TestPip',
|
'name': 'TestPip',
|
||||||
'module': 'dummy',
|
'module': 'mynoop',
|
||||||
'description': None,
|
'description': None,
|
||||||
'requirements': ['noop'],
|
'requirements': ['noop'],
|
||||||
'version': 0
|
'version': 0
|
||||||
}
|
}
|
||||||
root = os.path.join(self.dir, 'plugins', 'dummy_plugin')
|
module = plugins.from_info(info, root=self.examples_dir, install=True)
|
||||||
module = plugins.load_plugin_from_info(info, root=root)
|
|
||||||
plugins.install_deps(info)
|
|
||||||
assert module.name == 'TestPip'
|
assert module.name == 'TestPip'
|
||||||
assert module
|
assert module
|
||||||
import noop
|
import noop
|
||||||
@@ -65,8 +81,8 @@ class ExtensionsTest(TestCase):
|
|||||||
def test_enabling(self):
|
def test_enabling(self):
|
||||||
""" Enabling a plugin """
|
""" Enabling a plugin """
|
||||||
self.senpy.activate_all(sync=True)
|
self.senpy.activate_all(sync=True)
|
||||||
assert len(self.senpy.plugins) >= 3
|
assert len(self.senpy.plugins()) >= 3
|
||||||
assert self.senpy.plugins["Sleep"].is_activated
|
assert self.senpy.get_plugin("Sleep").is_activated
|
||||||
|
|
||||||
def test_installing_nonexistent(self):
|
def test_installing_nonexistent(self):
|
||||||
""" Fail if the dependencies cannot be met """
|
""" Fail if the dependencies cannot be met """
|
||||||
@@ -83,8 +99,8 @@ class ExtensionsTest(TestCase):
|
|||||||
def test_disabling(self):
|
def test_disabling(self):
|
||||||
""" Disabling a plugin """
|
""" Disabling a plugin """
|
||||||
self.senpy.deactivate_all(sync=True)
|
self.senpy.deactivate_all(sync=True)
|
||||||
assert not self.senpy.plugins["Dummy"].is_activated
|
assert not self.senpy.get_plugin("dummy").is_activated
|
||||||
assert not self.senpy.plugins["Sleep"].is_activated
|
assert not self.senpy.get_plugin("sleep").is_activated
|
||||||
|
|
||||||
def test_default(self):
|
def test_default(self):
|
||||||
""" Default plugin should be set """
|
""" Default plugin should be set """
|
||||||
@@ -107,7 +123,18 @@ class ExtensionsTest(TestCase):
|
|||||||
r2 = analyse(self.senpy, input="tupni", output="tuptuo")
|
r2 = analyse(self.senpy, input="tupni", output="tuptuo")
|
||||||
assert r1.analysis[0] == "plugins/Dummy_0.1"
|
assert r1.analysis[0] == "plugins/Dummy_0.1"
|
||||||
assert r2.analysis[0] == "plugins/Dummy_0.1"
|
assert r2.analysis[0] == "plugins/Dummy_0.1"
|
||||||
assert r1.entries[0]['nif:iString'] == 'input'
|
assert r1.entries[0]['nif:isString'] == 'input'
|
||||||
|
|
||||||
|
def test_analyse_empty(self):
|
||||||
|
""" Trying to analyse when no plugins are installed should raise an error."""
|
||||||
|
senpy = Senpy(plugin_folder=None,
|
||||||
|
app=self.app,
|
||||||
|
default_plugins=False)
|
||||||
|
self.assertRaises(Error, senpy.analyse, Results())
|
||||||
|
|
||||||
|
def test_analyse_wrong(self):
|
||||||
|
""" Trying to analyse with a non-existent plugin should raise an error."""
|
||||||
|
self.assertRaises(Error, analyse, self.senpy, algorithm='DOES NOT EXIST', input='test')
|
||||||
|
|
||||||
def test_analyse_jsonld(self):
|
def test_analyse_jsonld(self):
|
||||||
""" Using a plugin with JSON-LD input"""
|
""" Using a plugin with JSON-LD input"""
|
||||||
@@ -131,14 +158,15 @@ class ExtensionsTest(TestCase):
|
|||||||
output="tuptuo")
|
output="tuptuo")
|
||||||
assert r1.analysis[0] == "plugins/Dummy_0.1"
|
assert r1.analysis[0] == "plugins/Dummy_0.1"
|
||||||
assert r2.analysis[0] == "plugins/Dummy_0.1"
|
assert r2.analysis[0] == "plugins/Dummy_0.1"
|
||||||
assert r1.entries[0]['nif:iString'] == 'input'
|
assert r1.entries[0]['nif:isString'] == 'input'
|
||||||
|
|
||||||
def test_analyse_error(self):
|
def test_analyse_error(self):
|
||||||
mm = mock.MagicMock()
|
mm = mock.MagicMock()
|
||||||
mm.id = 'magic_mock'
|
mm.id = 'magic_mock'
|
||||||
|
mm.name = 'mock'
|
||||||
mm.is_activated = True
|
mm.is_activated = True
|
||||||
mm.analyse_entries.side_effect = Error('error in analysis', status=500)
|
mm.analyse_entries.side_effect = Error('error in analysis', status=500)
|
||||||
self.senpy.plugins['MOCK'] = mm
|
self.senpy.add_plugin(mm)
|
||||||
try:
|
try:
|
||||||
analyse(self.senpy, input='nothing', algorithm='MOCK')
|
analyse(self.senpy, input='nothing', algorithm='MOCK')
|
||||||
assert False
|
assert False
|
||||||
@@ -146,29 +174,28 @@ class ExtensionsTest(TestCase):
|
|||||||
assert 'error in analysis' in ex['message']
|
assert 'error in analysis' in ex['message']
|
||||||
assert ex['status'] == 500
|
assert ex['status'] == 500
|
||||||
|
|
||||||
mm.analyse.side_effect = Exception('generic exception on analysis')
|
ex = Exception('generic exception on analysis')
|
||||||
mm.analyse_entries.side_effect = Exception(
|
mm.analyse.side_effect = ex
|
||||||
'generic exception on analysis')
|
mm.analyse_entries.side_effect = ex
|
||||||
|
|
||||||
try:
|
try:
|
||||||
analyse(self.senpy, input='nothing', algorithm='MOCK')
|
analyse(self.senpy, input='nothing', algorithm='MOCK')
|
||||||
assert False
|
assert False
|
||||||
except Error as ex:
|
except Exception as ex:
|
||||||
assert 'generic exception on analysis' in ex['message']
|
assert 'generic exception on analysis' in str(ex)
|
||||||
assert ex['status'] == 500
|
|
||||||
|
|
||||||
def test_filtering(self):
|
def test_filtering(self):
|
||||||
""" Filtering plugins """
|
""" Filtering plugins """
|
||||||
assert len(self.senpy.filter_plugins(name="Dummy")) > 0
|
assert len(self.senpy.plugins(name="Dummy")) > 0
|
||||||
assert not len(self.senpy.filter_plugins(name="notdummy"))
|
assert not len(self.senpy.plugins(name="NotDummy"))
|
||||||
assert self.senpy.filter_plugins(name="Dummy", is_activated=True)
|
assert self.senpy.plugins(name="Dummy", is_activated=True)
|
||||||
self.senpy.deactivate_plugin("Dummy", sync=True)
|
self.senpy.deactivate_plugin("Dummy", sync=True)
|
||||||
assert not len(
|
assert not len(self.senpy.plugins(name="Dummy",
|
||||||
self.senpy.filter_plugins(name="Dummy", is_activated=True))
|
is_activated=True))
|
||||||
|
|
||||||
def test_load_default_plugins(self):
|
def test_load_default_plugins(self):
|
||||||
senpy = Senpy(plugin_folder=self.dir, default_plugins=True)
|
senpy = Senpy(plugin_folder=self.examples_dir, default_plugins=True)
|
||||||
assert len(senpy.plugins) > 1
|
assert len(senpy.plugins()) > 1
|
||||||
|
|
||||||
def test_convert_emotions(self):
|
def test_convert_emotions(self):
|
||||||
self.senpy.activate_all(sync=True)
|
self.senpy.activate_all(sync=True)
|
||||||
@@ -186,7 +213,7 @@ class ExtensionsTest(TestCase):
|
|||||||
response = Results({
|
response = Results({
|
||||||
'analysis': [{'plugin': plugin}],
|
'analysis': [{'plugin': plugin}],
|
||||||
'entries': [Entry({
|
'entries': [Entry({
|
||||||
'nif:iString': 'much ado about nothing',
|
'nif:isString': 'much ado about nothing',
|
||||||
'emotions': [eSet1]
|
'emotions': [eSet1]
|
||||||
})]
|
})]
|
||||||
})
|
})
|
||||||
|
@@ -9,13 +9,15 @@ from senpy.models import (Emotion,
|
|||||||
EmotionAnalysis,
|
EmotionAnalysis,
|
||||||
EmotionSet,
|
EmotionSet,
|
||||||
Entry,
|
Entry,
|
||||||
|
Entity,
|
||||||
Error,
|
Error,
|
||||||
Results,
|
Results,
|
||||||
Sentiment,
|
Sentiment,
|
||||||
|
SentimentPlugin,
|
||||||
Plugins,
|
Plugins,
|
||||||
Plugin,
|
|
||||||
from_string,
|
from_string,
|
||||||
from_dict)
|
from_dict,
|
||||||
|
subtypes)
|
||||||
from senpy import plugins
|
from senpy import plugins
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
@@ -99,7 +101,8 @@ class ModelsTest(TestCase):
|
|||||||
|
|
||||||
def test_plugins(self):
|
def test_plugins(self):
|
||||||
self.assertRaises(Error, plugins.Plugin)
|
self.assertRaises(Error, plugins.Plugin)
|
||||||
p = plugins.Plugin({"name": "dummy",
|
p = plugins.SentimentPlugin({"name": "dummy",
|
||||||
|
"description": "I do nothing",
|
||||||
"version": 0,
|
"version": 0,
|
||||||
"extra_params": {
|
"extra_params": {
|
||||||
"none": {
|
"none": {
|
||||||
@@ -110,7 +113,7 @@ class ModelsTest(TestCase):
|
|||||||
}})
|
}})
|
||||||
c = p.jsonld()
|
c = p.jsonld()
|
||||||
assert '@type' in c
|
assert '@type' in c
|
||||||
assert c['@type'] == 'plugin'
|
assert c['@type'] == 'sentimentPlugin'
|
||||||
assert 'info' not in c
|
assert 'info' not in c
|
||||||
assert 'repo' not in c
|
assert 'repo' not in c
|
||||||
assert 'extra_params' in c
|
assert 'extra_params' in c
|
||||||
@@ -123,7 +126,9 @@ class ModelsTest(TestCase):
|
|||||||
def test_str(self):
|
def test_str(self):
|
||||||
"""The string representation shouldn't include private variables"""
|
"""The string representation shouldn't include private variables"""
|
||||||
r = Results()
|
r = Results()
|
||||||
p = plugins.Plugin({"name": "STR test", "version": 0})
|
p = plugins.Plugin({"name": "STR test",
|
||||||
|
"description": "Test of private variables.",
|
||||||
|
"version": 0})
|
||||||
p._testing = 0
|
p._testing = 0
|
||||||
s = str(p)
|
s = str(p)
|
||||||
assert "_testing" not in s
|
assert "_testing" not in s
|
||||||
@@ -131,6 +136,11 @@ class ModelsTest(TestCase):
|
|||||||
s = str(r)
|
s = str(r)
|
||||||
assert "_testing" not in s
|
assert "_testing" not in s
|
||||||
|
|
||||||
|
def test_serialize(self):
|
||||||
|
for k, v in subtypes().items():
|
||||||
|
e = v()
|
||||||
|
e.serialize()
|
||||||
|
|
||||||
def test_turtle(self):
|
def test_turtle(self):
|
||||||
"""Any model should be serializable as a turtle file"""
|
"""Any model should be serializable as a turtle file"""
|
||||||
ana = EmotionAnalysis()
|
ana = EmotionAnalysis()
|
||||||
@@ -170,13 +180,14 @@ class ModelsTest(TestCase):
|
|||||||
def test_single_plugin(self):
|
def test_single_plugin(self):
|
||||||
"""A response with a single plugin should still return a list"""
|
"""A response with a single plugin should still return a list"""
|
||||||
plugs = Plugins()
|
plugs = Plugins()
|
||||||
p = Plugin({'id': str(1),
|
p = SentimentPlugin({'id': str(1),
|
||||||
'version': 0,
|
'version': 0,
|
||||||
'description': 'dummy'})
|
'description': 'dummy'})
|
||||||
plugs.plugins.append(p)
|
plugs.plugins.append(p)
|
||||||
assert isinstance(plugs.plugins, list)
|
assert isinstance(plugs.plugins, list)
|
||||||
js = plugs.jsonld()
|
js = plugs.jsonld()
|
||||||
assert isinstance(js['plugins'], list)
|
assert isinstance(js['plugins'], list)
|
||||||
|
assert js['plugins'][0]['@type'] == 'sentimentPlugin'
|
||||||
|
|
||||||
def test_from_string(self):
|
def test_from_string(self):
|
||||||
results = {
|
results = {
|
||||||
@@ -189,6 +200,7 @@ class ModelsTest(TestCase):
|
|||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
recovered = from_dict(results)
|
recovered = from_dict(results)
|
||||||
|
assert recovered.id == results['@id']
|
||||||
assert isinstance(recovered, Results)
|
assert isinstance(recovered, Results)
|
||||||
assert isinstance(recovered.entries[0], Entry)
|
assert isinstance(recovered.entries[0], Entry)
|
||||||
|
|
||||||
@@ -196,3 +208,14 @@ class ModelsTest(TestCase):
|
|||||||
recovered = from_string(string)
|
recovered = from_string(string)
|
||||||
assert isinstance(recovered, Results)
|
assert isinstance(recovered, Results)
|
||||||
assert isinstance(recovered.entries[0], Entry)
|
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,17 +1,25 @@
|
|||||||
#!/bin/env python
|
#!/bin/env python
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import pickle
|
import pickle
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase, skipIf
|
||||||
from senpy.models import Results, Entry, EmotionSet, Emotion
|
from senpy.models import Results, Entry, EmotionSet, Emotion, Plugins
|
||||||
from senpy import plugins
|
from senpy import plugins
|
||||||
from senpy.plugins.conversion.emotion.centroids import CentroidConversion
|
from senpy.plugins.conversion.emotion.centroids import CentroidConversion
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
class ShelfDummyPlugin(plugins.SentimentPlugin, plugins.ShelfMixin):
|
class ShelfDummyPlugin(plugins.SentimentPlugin, plugins.ShelfMixin):
|
||||||
|
'''Dummy plugin for tests.'''
|
||||||
|
name = 'Shelf'
|
||||||
|
version = 0
|
||||||
|
author = 'the senpy community'
|
||||||
|
|
||||||
def activate(self, *args, **kwargs):
|
def activate(self, *args, **kwargs):
|
||||||
if 'counter' not in self.sh:
|
if 'counter' not in self.sh:
|
||||||
self.sh['counter'] = 0
|
self.sh['counter'] = 0
|
||||||
@@ -40,19 +48,50 @@ class PluginsTest(TestCase):
|
|||||||
self.shelf_dir = tempfile.mkdtemp()
|
self.shelf_dir = tempfile.mkdtemp()
|
||||||
self.shelf_file = os.path.join(self.shelf_dir, "shelf")
|
self.shelf_file = os.path.join(self.shelf_dir, "shelf")
|
||||||
|
|
||||||
|
def test_serialize(self):
|
||||||
|
'''A plugin should be serializable and de-serializable'''
|
||||||
|
dummy = ShelfDummyPlugin()
|
||||||
|
dummy.serialize()
|
||||||
|
|
||||||
|
def test_jsonld(self):
|
||||||
|
'''A plugin should be serializable and de-serializable'''
|
||||||
|
dummy = ShelfDummyPlugin()
|
||||||
|
dummy.jsonld()
|
||||||
|
|
||||||
def test_shelf_file(self):
|
def test_shelf_file(self):
|
||||||
a = ShelfDummyPlugin(
|
a = ShelfDummyPlugin(
|
||||||
info={'name': 'default_shelve_file',
|
info={'name': 'default_shelve_file',
|
||||||
|
'description': 'Dummy plugin for tests',
|
||||||
'version': 'test'})
|
'version': 'test'})
|
||||||
a.activate()
|
a.activate()
|
||||||
assert os.path.isfile(a.shelf_file)
|
assert os.path.isfile(a.shelf_file)
|
||||||
os.remove(a.shelf_file)
|
os.remove(a.shelf_file)
|
||||||
|
|
||||||
|
def test_plugin_filter(self):
|
||||||
|
ps = Plugins()
|
||||||
|
for i in (plugins.SentimentPlugin,
|
||||||
|
plugins.EmotionPlugin,
|
||||||
|
plugins.AnalysisPlugin):
|
||||||
|
p = i(name='Plugin_{}'.format(i.__name__),
|
||||||
|
description='TEST',
|
||||||
|
version=0,
|
||||||
|
author='NOBODY')
|
||||||
|
ps.plugins.append(p)
|
||||||
|
assert len(ps.plugins) == 3
|
||||||
|
cases = [('AnalysisPlugin', 3),
|
||||||
|
('SentimentPlugin', 1),
|
||||||
|
('EmotionPlugin', 1)]
|
||||||
|
|
||||||
|
for name, num in cases:
|
||||||
|
res = list(plugins.pfilter(ps.plugins, plugin_type=name))
|
||||||
|
assert len(res) == num
|
||||||
|
|
||||||
def test_shelf(self):
|
def test_shelf(self):
|
||||||
''' A shelf is created and the value is stored '''
|
''' A shelf is created and the value is stored '''
|
||||||
newfile = self.shelf_file + "new"
|
newfile = self.shelf_file + "new"
|
||||||
a = ShelfDummyPlugin(info={
|
a = ShelfDummyPlugin(info={
|
||||||
'name': 'shelve',
|
'name': 'shelve',
|
||||||
|
'description': 'Shelf plugin for tests',
|
||||||
'version': 'test',
|
'version': 'test',
|
||||||
'shelf_file': newfile
|
'shelf_file': newfile
|
||||||
})
|
})
|
||||||
@@ -75,6 +114,7 @@ class PluginsTest(TestCase):
|
|||||||
pickle.dump({'counter': 99}, f)
|
pickle.dump({'counter': 99}, f)
|
||||||
a = ShelfDummyPlugin(info={
|
a = ShelfDummyPlugin(info={
|
||||||
'name': 'DummyShelf',
|
'name': 'DummyShelf',
|
||||||
|
'description': 'Dummy plugin for tests',
|
||||||
'shelf_file': self.shelf_file,
|
'shelf_file': self.shelf_file,
|
||||||
'version': 'test'
|
'version': 'test'
|
||||||
})
|
})
|
||||||
@@ -105,7 +145,8 @@ class PluginsTest(TestCase):
|
|||||||
with open(fn, 'rb') as f:
|
with open(fn, 'rb') as f:
|
||||||
msg, error = files[fn]
|
msg, error = files[fn]
|
||||||
a = ShelfDummyPlugin(info={
|
a = ShelfDummyPlugin(info={
|
||||||
'name': 'shelve',
|
'name': 'test_corrupt_shelf_{}'.format(msg),
|
||||||
|
'description': 'Dummy plugin for tests',
|
||||||
'version': 'test',
|
'version': 'test',
|
||||||
'shelf_file': f.name
|
'shelf_file': f.name
|
||||||
})
|
})
|
||||||
@@ -126,6 +167,7 @@ class PluginsTest(TestCase):
|
|||||||
''' Reusing the values of a previous shelf '''
|
''' Reusing the values of a previous shelf '''
|
||||||
a = ShelfDummyPlugin(info={
|
a = ShelfDummyPlugin(info={
|
||||||
'name': 'shelve',
|
'name': 'shelve',
|
||||||
|
'description': 'Dummy plugin for tests',
|
||||||
'version': 'test',
|
'version': 'test',
|
||||||
'shelf_file': self.shelf_file
|
'shelf_file': self.shelf_file
|
||||||
})
|
})
|
||||||
@@ -136,6 +178,7 @@ class PluginsTest(TestCase):
|
|||||||
|
|
||||||
b = ShelfDummyPlugin(info={
|
b = ShelfDummyPlugin(info={
|
||||||
'name': 'shelve',
|
'name': 'shelve',
|
||||||
|
'description': 'Dummy plugin for tests',
|
||||||
'version': 'test',
|
'version': 'test',
|
||||||
'shelf_file': self.shelf_file
|
'shelf_file': self.shelf_file
|
||||||
})
|
})
|
||||||
@@ -148,6 +191,7 @@ class PluginsTest(TestCase):
|
|||||||
''' Should be able to set extra parameters'''
|
''' Should be able to set extra parameters'''
|
||||||
a = ShelfDummyPlugin(info={
|
a = ShelfDummyPlugin(info={
|
||||||
'name': 'shelve',
|
'name': 'shelve',
|
||||||
|
'description': 'Dummy shelf plugin for tests',
|
||||||
'version': 'test',
|
'version': 'test',
|
||||||
'shelf_file': self.shelf_file,
|
'shelf_file': self.shelf_file,
|
||||||
'extra_params': {
|
'extra_params': {
|
||||||
@@ -160,6 +204,61 @@ class PluginsTest(TestCase):
|
|||||||
})
|
})
|
||||||
assert 'example' in a.extra_params
|
assert 'example' in a.extra_params
|
||||||
|
|
||||||
|
def test_box(self):
|
||||||
|
|
||||||
|
class MyBox(plugins.Box):
|
||||||
|
''' Vague description'''
|
||||||
|
|
||||||
|
author = 'me'
|
||||||
|
version = 0
|
||||||
|
|
||||||
|
def input(self, entry, **kwargs):
|
||||||
|
return entry.text
|
||||||
|
|
||||||
|
def predict_one(self, input):
|
||||||
|
return 'SIGN' in input
|
||||||
|
|
||||||
|
def output(self, output, entry, **kwargs):
|
||||||
|
if output:
|
||||||
|
entry.myAnnotation = 'DETECTED'
|
||||||
|
return entry
|
||||||
|
|
||||||
|
test_cases = [
|
||||||
|
{
|
||||||
|
'input': "nothing here",
|
||||||
|
'expected': {'myAnnotation': 'DETECTED'},
|
||||||
|
'should_fail': True
|
||||||
|
}, {
|
||||||
|
'input': "SIGN",
|
||||||
|
'expected': {'myAnnotation': 'DETECTED'}
|
||||||
|
}]
|
||||||
|
|
||||||
|
MyBox().test()
|
||||||
|
|
||||||
|
def test_sentimentbox(self):
|
||||||
|
|
||||||
|
class SentimentBox(plugins.MappingMixin, plugins.SentimentBox):
|
||||||
|
''' Vague description'''
|
||||||
|
|
||||||
|
author = 'me'
|
||||||
|
version = 0
|
||||||
|
|
||||||
|
mappings = {'happy': 'marl:Positive', 'sad': 'marl:Negative'}
|
||||||
|
|
||||||
|
def predict_one(self, input, **kwargs):
|
||||||
|
return 'happy' if ':)' in input else 'sad'
|
||||||
|
|
||||||
|
test_cases = [
|
||||||
|
{
|
||||||
|
'input': 'a happy face :)',
|
||||||
|
'polarity': 'marl:Positive'
|
||||||
|
}, {
|
||||||
|
'input': "Nothing",
|
||||||
|
'polarity': 'marl:Negative'
|
||||||
|
}]
|
||||||
|
|
||||||
|
SentimentBox().test()
|
||||||
|
|
||||||
def test_conversion_centroids(self):
|
def test_conversion_centroids(self):
|
||||||
info = {
|
info = {
|
||||||
"name": "CentroidTest",
|
"name": "CentroidTest",
|
||||||
@@ -213,21 +312,57 @@ class PluginsTest(TestCase):
|
|||||||
res = c._backwards_conversion(e)
|
res = c._backwards_conversion(e)
|
||||||
assert res["onyx:hasEmotionCategory"] == "c2"
|
assert res["onyx:hasEmotionCategory"] == "c2"
|
||||||
|
|
||||||
|
@skipIf(sys.version_info < (3, 0),
|
||||||
|
reason="requires Python3")
|
||||||
|
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'])
|
||||||
|
|
||||||
def make_mini_test(plugin_info):
|
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 make_mini_test(fpath):
|
||||||
def mini_test(self):
|
def mini_test(self):
|
||||||
plugin = plugins.load_plugin_from_info(plugin_info, install=True)
|
for plugin in plugins.from_path(fpath, install=True):
|
||||||
plugin.test()
|
plugin.test()
|
||||||
return mini_test
|
return mini_test
|
||||||
|
|
||||||
|
|
||||||
def _add_tests():
|
def _add_tests():
|
||||||
root = os.path.dirname(__file__)
|
root = os.path.join(os.path.dirname(__file__), '..')
|
||||||
plugs = plugins.load_plugins(os.path.join(root, ".."), loader=plugins.parse_plugin_info)
|
print(root)
|
||||||
for k, v in plugs.items():
|
for fpath in plugins.find_plugins([root, ]):
|
||||||
pass
|
pass
|
||||||
t_method = make_mini_test(v)
|
t_method = make_mini_test(fpath)
|
||||||
t_method.__name__ = 'test_plugin_{}'.format(k)
|
t_method.__name__ = 'test_plugin_{}'.format(fpath)
|
||||||
setattr(PluginsTest, t_method.__name__, t_method)
|
setattr(PluginsTest, t_method.__name__, t_method)
|
||||||
del t_method
|
del t_method
|
||||||
|
|
||||||
|
32
tests/test_test.py
Normal file
32
tests/test_test.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
from senpy.test import patch_requests
|
||||||
|
from senpy.models import Results
|
||||||
|
|
||||||
|
|
||||||
|
class TestTest(TestCase):
|
||||||
|
def test_patch_text(self):
|
||||||
|
with patch_requests('hello'):
|
||||||
|
r = requests.get('http://example.com')
|
||||||
|
assert r.text == 'hello'
|
||||||
|
assert r.content == 'hello'
|
||||||
|
|
||||||
|
def test_patch_json(self):
|
||||||
|
r = Results()
|
||||||
|
with patch_requests(r):
|
||||||
|
res = requests.get('http://example.com')
|
||||||
|
assert res.content == json.dumps(r.jsonld())
|
||||||
|
js = res.json()
|
||||||
|
assert js
|
||||||
|
assert js['@type'] == r['@type']
|
||||||
|
|
||||||
|
def test_patch_dict(self):
|
||||||
|
r = {'nothing': 'new'}
|
||||||
|
with patch_requests(r):
|
||||||
|
res = requests.get('http://example.com')
|
||||||
|
assert res.content == json.dumps(r)
|
||||||
|
js = res.json()
|
||||||
|
assert js
|
||||||
|
assert js['nothing'] == 'new'
|
Reference in New Issue
Block a user