1
0
mirror of https://github.com/gsi-upm/senpy synced 2025-10-20 10:18:26 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
J. Fernando Sánchez
87589e994a Fixed version.sh 2017-02-27 12:32:34 +01:00
40 changed files with 473 additions and 840 deletions

2
.gitignore vendored
View File

@@ -6,5 +6,3 @@ build
README.html README.html
__pycache__ __pycache__
VERSION VERSION
Dockerfile-*
Dockerfile

View File

@@ -1,4 +1,4 @@
image: gsiupm/dockermake:latest image: docker:latest
# When using dind, it's wise to use the overlayfs driver for # When using dind, it's wise to use the overlayfs driver for
@@ -6,60 +6,75 @@ image: gsiupm/dockermake:latest
variables: variables:
DOCKER_DRIVER: overlay DOCKER_DRIVER: overlay
DOCKERFILE: Dockerfile DOCKERFILE: Dockerfile
IMAGENAME: $CI_REGISTRY_IMAGE
before_script:
- sh version.sh > senpy/VERSION
stages: stages:
- test - test
- push - images
- clean - release
.test: &test_definition .test: &test_definition
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache"
cache:
paths:
- .eggs/
- "$CI_PROJECT_DIR/pip-cache"
- .venv
key: "$CI_PROJECT_NAME"
stage: test stage: test
script: script:
- make -e test-$PYTHON_VERSION - pip install --use-wheel -U pip setuptools virtualenv
- virtualenv .venv/$PYTHON_VERSION
- source .venv/$PYTHON_VERSION/bin/activate
- pip install --use-wheel -r requirements.txt
- pip install --use-wheel -r test-requirements.txt
- py.test --cov=senpy --cov-report term-missing
- python
test-3.5: test-3.5:
<<: *test_definition <<: *test_definition
variables: image: "python:3.5"
PYTHON_VERSION: "3.5"
test-2.7: test-2.7:
<<: *test_definition <<: *test_definition
variables: image: "python:2.7"
PYTHON_VERSION: "2.7"
.image: &image_definition .image: &image_definition
stage: push variables:
PYTHON_VERSION: "3.5"
before_script: before_script:
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
script: script:
- make -e push-$PYTHON_VERSION - docker build -f Dockerfile-3.5 . -t $CI_REGISTRY_IMAGE:$CI_BUILD_REF_SLUG-$PYTHON_VERSION
- docker push $CI_REGISTRY_IMAGE:$CI_BUILD_REF_SLUG-$PYTHON_VERSION
stage: images
only: only:
- tags - tags
- triggers - triggers
push-3.5: image-3.5:
<<: *image_definition <<: *image_definition
variables: variables:
PYTHON_VERSION: "3.5" PYTHON_VERSION: "3.5"
push-2.7: image-2.7:
<<: *image_definition <<: *image_definition
variables: variables:
PYTHON_VERSION: "2.7" PYTHON_VERSION: "2.7"
push-latest: image-latest:
<<: *image_definition stage: release
variables: before_script:
PYTHON_VERSION: latest - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
script:
- docker build -f Dockerfile . -t $CI_REGISTRY_IMAGE
- docker tag $CI_REGISTRY_IMAGE $CI_REGISTRY_IMAGE:$CI_BUILD_REF_SLUG
- docker push $CI_REGISTRY_IMAGE
- docker push $CI_REGISTRY_IMAGE:$CI_BUILD_REF_SLUG
only: only:
- master - master
- triggers - triggers
clean :
stage: clean
script:
- make -e clean
only:
- master

View File

@@ -1,5 +0,0 @@
- repo: git://github.com/pre-commit/pre-commit-hooks
sha: e626cd57090d8df0be21e4df0f4e55cc3511d6ab
hooks:
- id: flake8
- id: check-json

View File

@@ -1,13 +1,8 @@
sudo: required
services:
- docker
language: python language: python
python:
env: - "2.7"
- PYV=2.7 - "3.4"
- PYV=3.4 - "3.5"
- PYV=3.5 install: "pip install -r requirements.txt"
# run nosetests - Tests # run nosetests - Tests
script: make test-$PYV script: nosetests

1
Dockerfile Symbolic link
View File

@@ -0,0 +1 @@
Dockerfile-3.5

21
Dockerfile-2.7 Normal file
View File

@@ -0,0 +1,21 @@
from python:2.7
RUN mkdir /cache/
ENV PIP_CACHE_DIR=/cache/
WORKDIR /usr/src/app
ADD requirements.txt /usr/src/app/
RUN pip install --use-wheel -r requirements.txt
ADD . /usr/src/app/
RUN pip install .
VOLUME /data/
RUN mkdir /senpy-plugins/
WORKDIR /senpy-plugins/
ONBUILD ADD . /senpy-plugins/
ONBUILD RUN python -m senpy --only-install -f /senpy-plugins
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]

21
Dockerfile-3.4 Normal file
View File

@@ -0,0 +1,21 @@
from python:3.4
RUN mkdir /cache/
ENV PIP_CACHE_DIR=/cache/
WORKDIR /usr/src/app
ADD requirements.txt /usr/src/app/
RUN pip install --use-wheel -r requirements.txt
ADD . /usr/src/app/
RUN pip install .
VOLUME /data/
RUN mkdir /senpy-plugins/
WORKDIR /senpy-plugins/
ONBUILD ADD . /senpy-plugins/
ONBUILD RUN python -m senpy --only-install -f /senpy-plugins
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]

21
Dockerfile-3.5 Normal file
View File

@@ -0,0 +1,21 @@
FROM python:3.5
RUN mkdir /cache/
ENV PIP_CACHE_DIR=/cache/
WORKDIR /usr/src/app
ADD requirements.txt /usr/src/app/
RUN pip install --use-wheel -r requirements.txt
ADD . /usr/src/app/
RUN pip install .
VOLUME /data/
RUN mkdir /senpy-plugins/
WORKDIR /senpy-plugins/
ONBUILD ADD . /senpy-plugins/
ONBUILD RUN python -m senpy --only-install -f /senpy-plugins
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]

33
Dockerfile.deps Normal file
View File

@@ -0,0 +1,33 @@
from python:2.7
RUN apt-get update
RUN apt-get -y install git
RUN mkdir -p /senpy-plugins
RUN apt-get -y install python-numpy
RUN apt-get -y install python-scipy
RUN apt-get -y install python-sklearn
RUN apt-get -y install python-gevent
RUN apt-get -y install libopenblas-dev
RUN apt-get -y install gfortran
RUN apt-get -y install libxml2-dev libxslt1-dev python-dev
#RUN pip install --upgrade pip
ADD id_rsa /root/.ssh/id_rsa
RUN chmod 700 /root/.ssh/id_rsa
RUN echo "Host github.com\n\tStrictHostKeyChecking no\n" >> /root/.ssh/config
RUN git clone https://github.com/gsi-upm/senpy /usr/src/app/
RUN git clone git@github.com:gsi-upm/senpy-plugins-enterprise /senpy-plugins/enterprise
RUN git clone https://github.com/gsi-upm/senpy-plugins-community /senpy-plugins/community
RUN pip install /usr/src/app
RUN pip install --no-use-wheel -r /senpy-plugins/enterprise/requirements.txt
RUN python -m nltk.downloader stopwords
RUN python -m nltk.downloader punkt
RUN python -m nltk.downloader maxent_treebank_pos_tagger
RUN python -m nltk.downloader wordnet
WORKDIR /senpy-plugins
ENTRYPOINT ["python", "-m", "senpy", "-f", ".", "--host", "0.0.0.0"]

View File

@@ -1,22 +1,21 @@
from python:{{PYVERSION}} from python:{{PYVERSION}}
MAINTAINER J. Fernando Sánchez <jf.sanchez@upm.es> RUN mkdir /cache/
ENV PIP_CACHE_DIR=/cache/
WORKDIR /usr/src/app
ADD requirements.txt /usr/src/app/
RUN pip install --use-wheel -r requirements.txt
ADD . /usr/src/app/
RUN pip install .
RUN mkdir /cache/ /senpy-plugins /data/
VOLUME /data/ VOLUME /data/
ENV PIP_CACHE_DIR=/cache/ SENPY_DATA=/data RUN mkdir /senpy-plugins/
ONBUILD COPY . /senpy-plugins/ WORKDIR /senpy-plugins/
ONBUILD ADD . /senpy-plugins/
ONBUILD RUN python -m senpy --only-install -f /senpy-plugins ONBUILD RUN python -m senpy --only-install -f /senpy-plugins
ONBUILD WORKDIR /senpy-plugins/
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]
WORKDIR /usr/src/app
COPY test-requirements.txt requirements.txt /usr/src/app/
RUN pip install --use-wheel -r test-requirements.txt -r requirements.txt
COPY . /usr/src/app/
RUN pip install --no-deps --no-index .
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]

View File

@@ -1,18 +1,17 @@
PYVERSIONS=3.5 2.7 PYVERSIONS=3.5 3.4 2.7
PYMAIN=$(firstword $(PYVERSIONS)) PYMAIN=$(firstword $(PYVERSIONS))
NAME=senpy NAME=senpy
REPO=gsiupm REPO=gsiupm
VERSION=$(shell git describe --tags --dirty 2>/dev/null) VERSION=$(shell ./version.sh)
TARNAME=$(NAME)-$(VERSION).tar.gz TARNAME=$(NAME)-$(VERSION).tar.gz
IMAGENAME=$(REPO)/$(NAME) IMAGENAME=$(REPO)/$(NAME):$(VERSION)
IMAGEWTAG=$(IMAGENAME):$(VERSION) TEST_COMMAND=gitlab-runner exec docker --cache-dir=/tmp/gitlabrunner --docker-volumes /tmp/gitlabrunner:/tmp/gitlabrunner --env CI_PROJECT_NAME=$(NAME)
action="test-${PYMAIN}"
all: build run all: build run
.FORCE: FORCE:
version: .FORCE version: FORCE
@echo $(VERSION) > $(NAME)/VERSION @echo $(VERSION) > $(NAME)/VERSION
@echo $(VERSION) @echo $(VERSION)
@@ -20,7 +19,7 @@ yapf:
yapf -i -r senpy yapf -i -r senpy
yapf -i -r tests yapf -i -r tests
init: dev:
pip install --user pre-commit pip install --user pre-commit
pre-commit install pre-commit install
@@ -35,27 +34,26 @@ quick_build: $(addprefix build-, $(PYMAIN))
build: $(addprefix build-, $(PYVERSIONS)) build: $(addprefix build-, $(PYVERSIONS))
build-%: version Dockerfile-% build-%: Dockerfile-%
docker build -t '$(IMAGEWTAG)-python$*' -f Dockerfile-$* .; docker build -t '$(IMAGENAME)-python$*' -f Dockerfile-$* .;
quick_test: $(addprefix test-,$(PYMAIN)) quick_test: $(addprefix test-,$(PYMAIN))
dev-%: test: $(addprefix test-,$(PYVERSIONS))
@docker start $(NAME)-dev$* || (\
debug-%:
@docker start $(NAME)-debug || (\
$(MAKE) build-$*; \ $(MAKE) build-$*; \
docker run -d -w /usr/src/app/ -v $$PWD:/usr/src/app --entrypoint=/bin/bash -ti --name $(NAME)-dev$* '$(IMAGEWTAG)-python$*'; \ docker run -d -w /usr/src/app/ -v $$PWD:/usr/src/app --entrypoint=/bin/bash -p 5000:5000 -ti --name $(NAME)-debug '$(IMAGENAME)-python$*'; \
docker exec -ti $(NAME)-debug pip install -r test-requirements.txt; \
)\ )\
docker exec -ti $(NAME)-dev$* bash docker attach $(NAME)-debug
dev: dev-$(PYMAIN) debug: debug-$(PYMAIN)
test-all: $(addprefix test-,$(PYVERSIONS)) test-%:
$(TEST_COMMAND) test-$*
test-%: build-%
docker run --rm --entrypoint /usr/local/bin/python -w /usr/src/app $(IMAGEWTAG)-python$* setup.py test
test: test-$(PYMAIN)
dist/$(TARNAME): dist/$(TARNAME):
docker run --rm -ti -v $$PWD:/usr/src/app/ -w /usr/src/app/ python:$(PYMAIN) python setup.py sdist; docker run --rm -ti -v $$PWD:/usr/src/app/ -w /usr/src/app/ python:$(PYMAIN) python setup.py sdist;
@@ -67,10 +65,19 @@ pip_test-%: sdist
pip_test: $(addprefix pip_test-,$(PYVERSIONS)) pip_test: $(addprefix pip_test-,$(PYVERSIONS))
upload-%: test-%
docker push '$(IMAGENAME)-python$*'
upload: test $(addprefix upload-,$(PYVERSIONS))
docker tag '$(IMAGENAME)-python$(PYMAIN)' '$(IMAGENAME)'
docker tag '$(IMAGENAME)-python$(PYMAIN)' '$(REPO)/$(NAME)'
docker push '$(IMAGENAME)'
docker push '$(REPO)/$(NAME)'
clean: clean:
@docker ps -a | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1;}}' | xargs docker rm -v 2>/dev/null|| true @docker ps -a | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1;}}' | xargs docker rm -v 2>/dev/null|| true
@docker images | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1":"$$2;}}' | xargs docker rmi 2>/dev/null|| true @docker images | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1":"$$2;}}' | xargs docker rmi 2>/dev/null|| true
@docker rmi $(NAME)-dev 2>/dev/null || true @docker rmi $(NAME)-debug 2>/dev/null || true
git_commit: git_commit:
@@ -79,31 +86,17 @@ git_commit:
git_tag: git_tag:
git tag ${VERSION} git tag ${VERSION}
git_push: upload_git:
git push --tags origin master git push --tags origin master
pip_upload: pip_upload:
python setup.py sdist upload ; python setup.py sdist upload ;
pip_test: $(addprefix pip_test-,$(PYVERSIONS))
run-%: build-% run-%: build-%
docker run --rm -p 5000:5000 -ti '$(IMAGEWTAG)-python$(PYMAIN)' --default-plugins docker run --rm -p 5000:5000 -ti '$(IMAGENAME)-python$(PYMAIN)' --default-plugins
run: run-$(PYMAIN) run: run-$(PYMAIN)
push-latest: build-$(PYMAIN) .PHONY: test test-% build-% build test pip_test run yapf dev
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGEWTAG)'
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGENAME)'
docker push '$(IMAGENAME):latest'
docker push '$(IMAGEWTAG)'
push-%: build-%
docker push $(IMAGENAME):$(VERSION)-python$*
push: $(addprefix push-,$(PYVERSIONS))
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGEWTAG)'
docker push $(IMAGENAME):$(VERSION)
ci:
gitlab-runner exec docker --docker-volumes /var/run/docker.sock:/var/run/docker.sock --env CI_PROJECT_NAME=$(NAME) ${action}
.PHONY: test test-% test-all build-% build test pip_test run yapf push-main push-% dev ci version .FORCE

View File

@@ -23,7 +23,7 @@ Through PIP
.. code:: bash .. code:: bash
pip install -U --user senpy pip install --user senpy
Alternatively, you can use the development version: Alternatively, you can use the development version:
@@ -38,56 +38,9 @@ If you want to install senpy globally, use sudo instead of the ``--user`` flag.
Docker Image Docker Image
************ ************
Build the image or use the pre-built one: ``docker run -ti -p 5000:5000 gsiupm/senpy --default-plugins``. Build the image or use the pre-built one: ``docker run -ti -p 5000:5000 gsiupm/senpy --host 0.0.0.0 --default-plugins``.
To add custom plugins, add a volume and tell senpy where to find the plugins: ``docker run -ti -p 5000:5000 -v <PATH OF PLUGINS>:/plugins gsiupm/senpy --default-plugins -f /plugins`` To add custom plugins, add a volume and tell senpy where to find the plugins: ``docker run -ti -p 5000:5000 -v <PATH OF PLUGINS>:/plugins gsiupm/senpy --host 0.0.0.0 --default-plugins -f /plugins``
Developing
----------
Developing/debugging
********************
This command will run the senpy container using the latest image available, mounting your current folder so you get your latest code:
.. code:: bash
# Python 3.5
make dev
# Python 2.7
make dev-2.7
Building a docker image
***********************
.. code:: bash
# Python 3.5
make build-3.5
# Python 2.7
make build-2.7
Testing
*******
.. code:: bash
make test
Running
*******
This command will run the senpy server listening on localhost:5000
.. code:: bash
# Python 3.5
make run-3.5
# Python 2.7
make run-2.7
Usage Usage
----- -----
@@ -96,14 +49,12 @@ However, the easiest and recommended way is to just use the command-line tool to
.. code:: bash .. code:: bash
senpy senpy
or, alternatively: or, alternatively:
.. code:: bash .. code:: bash
python -m senpy python -m senpy

View File

@@ -62,7 +62,6 @@ NIF API
:query o outformat: one of `turtle` (default), `text`, `json-ld` :query o outformat: one of `turtle` (default), `text`, `json-ld`
:query p prefix: prefix for the URIs :query p prefix: prefix for the URIs
:query algo algorithm: algorithm/plugin to use for the analysis. For a list of options, see :http:get:`/api/plugins`. If not provided, the default plugin will be used (:http:get:`/api/plugins/default`). :query algo algorithm: algorithm/plugin to use for the analysis. For a list of options, see :http:get:`/api/plugins`. If not provided, the default plugin will be used (:http:get:`/api/plugins/default`).
:query algo emotionModel: desired emotion model in the results. If the requested algorithm does not use that emotion model, there are conversion plugins specifically for this. If none of the plugins match, an error will be returned, which includes the results *as is*.
:reqheader Accept: the response content type depends on :reqheader Accept: the response content type depends on
:mailheader:`Accept` header :mailheader:`Accept` header
@@ -70,7 +69,6 @@ NIF API
header of request header of request
:statuscode 200: no error :statuscode 200: no error
:statuscode 404: service not found :statuscode 404: service not found
:statuscode 400: error while processing the request
.. http:post:: /api .. http:post:: /api
@@ -95,53 +93,51 @@ NIF API
{ {
"@context": { "@context": {
... ...
}, },
"@type": "plugins", "sentiment140": {
"plugins": [ "name": "sentiment140",
{ "is_activated": true,
"name": "sentiment140", "version": "0.1",
"is_activated": true, "extra_params": {
"version": "0.1", "@id": "extra_params_sentiment140_0.1",
"extra_params": { "language": {
"@id": "extra_params_sentiment140_0.1", "required": false,
"language": { "@id": "lang_sentiment140",
"required": false, "options": [
"@id": "lang_sentiment140", "es",
"options": [ "en",
"es", "auto"
"en", ],
"auto" "aliases": [
], "language",
"aliases": [ "l"
"language", ]
"l" }
] },
} "@id": "sentiment140_0.1"
}, },
"@id": "sentiment140_0.1" "rand": {
}, { "name": "rand",
"name": "rand", "is_activated": true,
"is_activated": true, "version": "0.1",
"version": "0.1", "extra_params": {
"extra_params": { "@id": "extra_params_rand_0.1",
"@id": "extra_params_rand_0.1", "language": {
"language": { "required": false,
"required": false, "@id": "lang_rand",
"@id": "lang_rand", "options": [
"options": [ "es",
"es", "en",
"en", "auto"
"auto" ],
], "aliases": [
"aliases": [ "language",
"language", "l"
"l" ]
] }
} },
}, "@id": "rand_0.1"
"@id": "rand_0.1" }
}
]
} }
@@ -152,7 +148,7 @@ NIF API
.. sourcecode:: http .. sourcecode:: http
GET /api/plugins/rand/ HTTP/1.1 GET /api/plugins/rand HTTP/1.1
Host: localhost Host: localhost
Accept: application/json, text/javascript Accept: application/json, text/javascript
@@ -163,7 +159,6 @@ NIF API
{ {
"@id": "rand_0.1", "@id": "rand_0.1",
"@type": "sentimentPlugin",
"extra_params": { "extra_params": {
"@id": "extra_params_rand_0.1", "@id": "extra_params_rand_0.1",
"language": { "language": {
@@ -190,3 +185,24 @@ NIF API
Return the information about the default plugin. Return the information about the default plugin.
.. http:get:: /api/plugins/<pluginname>/{de}activate
{De}activate a plugin.
**Example request**:
.. sourcecode:: http
GET /api/plugins/rand/deactivate HTTP/1.1
Host: localhost
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
{
"@context": {},
"message": "Ok"
}

View File

@@ -1,116 +0,0 @@
Conversion
----------
Senpy includes experimental support for emotion/sentiment conversion plugins.
Use
===
Consider the original query: http://127.0.0.1:5000/api/?i=hello&algo=emoRand
The requested plugin (emoRand) returns emotions using Ekman's model (or big6 in EmotionML):
.. code:: json
... rest of the document ...
{
"@type": "emotionSet",
"onyx:hasEmotion": {
"@type": "emotion",
"onyx:hasEmotionCategory": "emoml:big6anger"
},
"prov:wasGeneratedBy": "plugins/emoRand_0.1"
}
To get these emotions in VAD space (FSRE dimensions in EmotionML), we'd do this:
http://127.0.0.1:5000/api/?i=hello&algo=emoRand&emotionModel=emoml:fsre-dimensions
This call, provided there is a valid conversion plugin from Ekman's to VAD, would return something like this:
.. code:: json
... rest of the document ...
{
"@type": "emotionSet",
"onyx:hasEmotion": {
"@type": "emotion",
"onyx:hasEmotionCategory": "emoml:big6anger"
},
"prov:wasGeneratedBy": "plugins/emoRand_0.1"
}, {
"@type": "emotionSet",
"onyx:hasEmotion": {
"@type": "emotion",
"A": 7.22,
"D": 6.28,
"V": 8.6
},
"prov:wasGeneratedBy": "plugins/Ekman2VAD_0.1"
}
That is called a *full* response, as it simply adds the converted emotion alongside.
It is also possible to get the original emotion nested within the new converted emotion, using the `conversion=nested` parameter:
.. code:: json
... rest of the document ...
{
"@type": "emotionSet",
"onyx:hasEmotion": {
"@type": "emotion",
"onyx:hasEmotionCategory": "emoml:big6anger"
},
"prov:wasGeneratedBy": "plugins/emoRand_0.1"
"onyx:wasDerivedFrom": {
"@type": "emotionSet",
"onyx:hasEmotion": {
"@type": "emotion",
"A": 7.22,
"D": 6.28,
"V": 8.6
},
"prov:wasGeneratedBy": "plugins/Ekman2VAD_0.1"
}
}
Lastly, `conversion=filtered` would only return the converted emotions.
Developing a conversion plugin
================================
Conversion plugins are discovered by the server just like any other plugin.
The difference is the slightly different API, and the need to specify the `source` and `target` of the conversion.
For instance, an emotion conversion plugin needs the following:
.. code:: yaml
---
onyx:doesConversion:
- onyx:conversionFrom: emoml:big6
onyx:conversionTo: emoml:fsre-dimensions
- onyx:conversionFrom: emoml:fsre-dimensions
onyx:conversionTo: emoml:big6
.. code:: python
class MyConversion(EmotionConversionPlugin):
def convert(self, emotionSet, fromModel, toModel, params):
pass

View File

@@ -1,3 +1,8 @@
.. Senpy documentation master file, created by
sphinx-quickstart on Tue Feb 24 08:57:32 2015.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to Senpy's documentation! Welcome to Senpy's documentation!
================================= =================================
@@ -10,6 +15,5 @@ Contents:
api api
schema schema
plugins plugins
conversion
demo demo
:maxdepth: 2 :maxdepth: 2

View File

@@ -1,7 +1,5 @@
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`.
Each plugin represents a different analysis process.There are two types of files that are needed by senpy for loading a plugin: Each plugin represents a different analysis process.There are two types of files that are needed by senpy for loading a plugin:
- Definition file, has the ".senpy" extension. - Definition file, has the ".senpy" extension.

View File

@@ -48,8 +48,8 @@ Once the server is launched, there is a basic endpoint in the server, which prov
In case you want to know the different endpoints of the server, there is more information available in the NIF API section_. In case you want to know the different endpoints of the server, there is more information available in the NIF API section_.
CLI Video example
=== =============
This video shows how to use senpy through command-line tool. This video shows how to use senpy through command-line tool.
@@ -58,23 +58,18 @@ https://asciinema.org/a/9uwef1ghkjk062cw2t4mhzpyk
Request example in python Request example in python
========================= =========================
This example shows how to make a request to the default plugin: This example shows how to make a request to a plugin.
.. code:: python .. code:: python
from senpy.client import Client import requests
import json
c = Client('http://127.0.0.1:5000/api/') r = requests.get('http://127.0.0.1:5000/api/?algo=rand&i=Testing')
r = c.analyse('hello world') response = r.content.decode('utf-8')
response_json = json.loads(response)
for entry in r.entries:
print('{} -> {}'.format(entry.text, entry.emotions))
.. _section: http://senpy.readthedocs.org/en/latest/api.html .. _section: http://senpy.readthedocs.org/en/latest/api.html
Conversion
==========
See :doc:`conversion`

View File

@@ -17,6 +17,7 @@
""" """
Sentiment analysis server in Python Sentiment analysis server in Python
""" """
from __future__ import print_function
from .version import __version__ from .version import __version__
import logging import logging

View File

@@ -26,13 +26,6 @@ API_PARAMS = {
"aliases": ["emotionModel", "emoModel"], "aliases": ["emotionModel", "emoModel"],
"required": False "required": False
}, },
"plugin_type": {
"@id": "pluginType",
"description": 'What kind of plugins to list',
"aliases": ["pluginType", "plugin_type"],
"required": True,
"default": "analysisPlugin"
},
"conversion": { "conversion": {
"@id": "conversion", "@id": "conversion",
"description": "How to show the elements that have (not) been converted", "description": "How to show the elements that have (not) been converted",

View File

@@ -113,20 +113,15 @@ def basic_api(f):
@api_blueprint.route('/', methods=['POST', 'GET']) @api_blueprint.route('/', methods=['POST', 'GET'])
@basic_api @basic_api
def api(): def api():
try: response = current_app.senpy.analyse(**request.params)
response = current_app.senpy.analyse(**request.params) return response
return response
except Error as ex:
return ex
@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.params.get('plugin_type') dic = Plugins(plugins=list(sp.plugins.values()))
plugins = sp.filter_plugins(plugin_type=ptype)
dic = Plugins(plugins=list(plugins.values()))
return dic return dic

View File

@@ -18,6 +18,7 @@ class Client(object):
try: try:
resp = models.from_dict(response.json()) resp = models.from_dict(response.json())
resp.validate(resp) resp.validate(resp)
return resp
except Exception as ex: except Exception as ex:
logger.error(('There seems to be a problem with the response:\n' logger.error(('There seems to be a problem with the response:\n'
'\tURL: {url}\n' '\tURL: {url}\n'
@@ -32,6 +33,3 @@ class Client(object):
code=response.status_code, code=response.status_code,
content=response.content)) content=response.content))
raise ex raise ex
if isinstance(resp, models.Error):
raise resp
return resp

View File

@@ -5,20 +5,18 @@ It orchestrates plugin (de)activation and analysis.
from future import standard_library from future import standard_library
standard_library.install_aliases() standard_library.install_aliases()
from . import plugins from .plugins import SentimentPlugin, SenpyPlugin
from .plugins import SenpyPlugin from .models import Error, Entry, Results
from .models import Error, Entry, Results, from_dict
from .blueprints import api_blueprint, demo_blueprint, ns_blueprint from .blueprints import api_blueprint, demo_blueprint, ns_blueprint
from .api import API_PARAMS, NIF_PARAMS, parse_params from .api import API_PARAMS, NIF_PARAMS, parse_params
from threading import Thread from threading import Thread
import os import os
import copy
import fnmatch import fnmatch
import inspect import inspect
import sys import sys
import importlib import imp
import logging import logging
import traceback import traceback
import yaml import yaml
@@ -78,101 +76,70 @@ class Senpy(object):
else: else:
logger.debug("Not a folder: %s", folder) logger.debug("Not a folder: %s", folder)
def _find_plugins(self, params): def _find_plugin(self, params):
if not self.analysis_plugins: api_params = parse_params(params, spec=API_PARAMS)
algo = None
if "algorithm" in api_params and api_params["algorithm"]:
algo = api_params["algorithm"]
elif self.plugins:
algo = self.default_plugin and self.default_plugin.name
if not algo:
raise Error( raise Error(
status=404, status=404,
message=("No plugins found." message=("No plugins found."
" Please install one.")) " Please install one.").format(algo))
api_params = parse_params(params, spec=API_PARAMS) if algo not in self.plugins:
algos = None logger.debug(("The algorithm '{}' is not valid\n"
if "algorithm" in api_params and api_params["algorithm"]: "Valid algorithms: {}").format(algo,
algos = api_params["algorithm"].split(',') self.plugins.keys()))
elif self.default_plugin:
algos = [self.default_plugin.name, ]
else:
raise Error( raise Error(
status=404, status=404,
message="No default plugin found, and None provided") message="The algorithm '{}' is not valid".format(algo))
plugins = list() if not self.plugins[algo].is_activated:
for algo in algos: logger.debug("Plugin not activated: {}".format(algo))
if algo not in self.plugins: raise Error(
logger.debug(("The algorithm '{}' is not valid\n" status=400,
"Valid algorithms: {}").format(algo, message=("The algorithm '{}'"
self.plugins.keys())) " is not activated yet").format(algo))
raise Error( return self.plugins[algo]
status=404,
message="The algorithm '{}' is not valid".format(algo))
if not self.plugins[algo].is_activated: def _get_params(self, params, plugin):
logger.debug("Plugin not activated: {}".format(algo))
raise Error(
status=400,
message=("The algorithm '{}'"
" is not activated yet").format(algo))
plugins.append(self.plugins[algo])
return plugins
def _get_params(self, params, plugin=None):
nif_params = parse_params(params, spec=NIF_PARAMS) nif_params = parse_params(params, spec=NIF_PARAMS)
if plugin: extra_params = plugin.get('extra_params', {})
extra_params = plugin.get('extra_params', {}) specific_params = parse_params(params, spec=extra_params)
specific_params = parse_params(params, spec=extra_params) nif_params.update(specific_params)
nif_params.update(specific_params)
return nif_params return nif_params
def _get_entries(self, params): def _get_entries(self, params):
entry = None
if params['informat'] == 'text': if params['informat'] == 'text':
results = Results()
entry = Entry(text=params['input']) entry = Entry(text=params['input'])
results.entries.append(entry)
elif params['informat'] == 'json-ld':
results = from_dict(params['input'])
else: else:
raise NotImplemented('Informat {} is not implemented'.format(params['informat'])) raise NotImplemented('Only text input format implemented')
return results yield entry
def _process_entries(self, entries, plugins, nif_params):
if not plugins:
for i in entries:
yield i
return
plugin = plugins[0]
specific_params = self._get_params(nif_params, plugin)
results = plugin.analyse_entries(entries, specific_params)
for i in self._process_entries(results, plugins[1:], nif_params):
yield i
def _process_response(self, resp, plugins, nif_params):
entries = resp.entries
resp.entries = []
for plug in plugins:
resp.analysis.append(plug.id)
for i in self._process_entries(entries, plugins, nif_params):
resp.entries.append(i)
return resp
def analyse(self, **api_params): def analyse(self, **api_params):
"""
Main method that analyses a request, either from CLI or HTTP.
It uses a dictionary of parameters, provided by the user.
"""
logger.debug("analysing with params: {}".format(api_params)) logger.debug("analysing with params: {}".format(api_params))
plugins = self._find_plugins(api_params) plugin = self._find_plugin(api_params)
nif_params = self._get_params(api_params) nif_params = self._get_params(api_params, plugin)
resp = self._get_entries(nif_params) resp = Results()
if 'with_parameters' in api_params: if 'with_parameters' in api_params:
resp.parameters = nif_params resp.parameters = nif_params
try: try:
resp = self._process_response(resp, plugins, nif_params) entries = []
self.convert_emotions(resp, plugins, nif_params) for i in self._get_entries(nif_params):
entries += list(plugin.analyse_entry(i, nif_params))
resp.entries = entries
self.convert_emotions(resp, plugin, nif_params)
resp.analysis.append(plugin.id)
logger.debug("Returning analysis result: {}".format(resp)) logger.debug("Returning analysis result: {}".format(resp))
except (Error, Exception) as ex: except Error as ex:
if not isinstance(ex, Error):
ex = Error(message=str(ex), status=500)
logger.exception('Error returning analysis result') logger.exception('Error returning analysis result')
raise ex resp = ex
except Exception as ex:
logger.exception('Error returning analysis result')
resp = Error(message=str(ex), status=500)
return resp return resp
def _conversion_candidates(self, fromModel, toModel): def _conversion_candidates(self, fromModel, toModel):
@@ -186,7 +153,7 @@ class Senpy(object):
# logging.debug('Found candidate: {}'.format(candidate)) # logging.debug('Found candidate: {}'.format(candidate))
yield candidate yield candidate
def convert_emotions(self, resp, plugins, params): def convert_emotions(self, resp, plugin, params):
""" """
Conversion of all emotions in a response. Conversion of all emotions in a response.
In addition to converting from one model to another, it has In addition to converting from one model to another, it has
@@ -194,35 +161,29 @@ class Senpy(object):
Needless to say, this is far from an elegant solution, but it works. Needless to say, this is far from an elegant solution, but it works.
@todo refactor and clean up @todo refactor and clean up
""" """
fromModel = plugin.get('onyx:usesEmotionModel', None)
toModel = params.get('emotionModel', None) toModel = params.get('emotionModel', None)
output = params.get('conversion', None)
logger.debug('Asked for model: {}'.format(toModel))
logger.debug('Analysis plugin uses model: {}'.format(fromModel))
if not toModel: if not toModel:
return return
try:
logger.debug('Asked for model: {}'.format(toModel)) candidate = next(self._conversion_candidates(fromModel, toModel))
output = params.get('conversion', None) except StopIteration:
candidates = {} e = Error(('No conversion plugin found for: '
for plugin in plugins: '{} -> {}'.format(fromModel, toModel)))
try: e.original_response = resp
fromModel = plugin.get('onyx:usesEmotionModel', None) e.parameters = params
candidates[plugin.id] = next(self._conversion_candidates(fromModel, toModel)) raise e
logger.debug('Analysis plugin {} uses model: {}'.format(plugin.id, fromModel))
except StopIteration:
e = Error(('No conversion plugin found for: '
'{} -> {}'.format(fromModel, toModel)))
e.original_response = resp
e.parameters = params
raise e
newentries = [] newentries = []
resp.analysis = set(resp.analysis)
for i in resp.entries: for i in resp.entries:
if output == "full": if output == "full":
newemotions = copy.deepcopy(i.emotions) newemotions = i.emotions.copy()
else: else:
newemotions = [] newemotions = []
for j in i.emotions: for j in i.emotions:
plugname = j['prov:wasGeneratedBy']
candidate = candidates[plugname]
resp.analysis.add(candidate.id)
for k in candidate.convert(j, fromModel, toModel, params): for k in candidate.convert(j, fromModel, toModel, params):
k.prov__wasGeneratedBy = candidate.id k.prov__wasGeneratedBy = candidate.id
if output == 'nested': if output == 'nested':
@@ -231,6 +192,7 @@ class Senpy(object):
i.emotions = newemotions i.emotions = newemotions
newentries.append(i) newentries.append(i)
resp.entries = newentries resp.entries = newentries
resp.analysis.append(candidate.id)
@property @property
def default_plugin(self): def default_plugin(self):
@@ -290,7 +252,7 @@ class Senpy(object):
logger.error(msg) logger.error(msg)
raise Error(msg) raise Error(msg)
if sync or 'async' in plugin and not plugin.async: if sync:
act() act()
else: else:
th = Thread(target=act) th = Thread(target=act)
@@ -314,7 +276,7 @@ class Senpy(object):
"Error deactivating plugin {}: {}".format(plugin.name, ex)) "Error deactivating plugin {}: {}".format(plugin.name, ex))
logger.error("Trace: {}".format(traceback.format_exc())) logger.error("Trace: {}".format(traceback.format_exc()))
if sync or 'async' in plugin and not plugin.async: if sync:
deact() deact()
else: else:
th = Thread(target=deact) th = Thread(target=deact)
@@ -322,11 +284,11 @@ class Senpy(object):
@classmethod @classmethod
def validate_info(cls, info): def validate_info(cls, info):
return all(x in info for x in ('name', 'module', 'description', 'version')) return all(x in info for x in ('name', 'module', 'version'))
def install_deps(self): def install_deps(self):
for i in self.plugins.values(): for i in self.plugins.values():
self._install_deps(i) self._install_deps(i._info)
@classmethod @classmethod
def _install_deps(cls, info=None): def _install_deps(cls, info=None):
@@ -340,13 +302,6 @@ class Senpy(object):
logger.info('Installing requirements: ' + str(requirements)) logger.info('Installing requirements: ' + str(requirements))
pip.main(pip_args) pip.main(pip_args)
@classmethod
def _load_module(cls, name, root):
sys.path.append(root)
tmp = importlib.import_module(name)
sys.path.remove(root)
return tmp
@classmethod @classmethod
def _load_plugin_from_info(cls, info, root): def _load_plugin_from_info(cls, info, root):
if not cls.validate_info(info): if not cls.validate_info(info):
@@ -354,10 +309,11 @@ class Senpy(object):
return None, None return None, None
module = info["module"] module = info["module"]
name = info["name"] name = info["name"]
sys.path.append(root)
(fp, pathname, desc) = imp.find_module(module, [root, ])
cls._install_deps(info) cls._install_deps(info)
tmp = cls._load_module(module, root) tmp = imp.load_module(module, fp, pathname, desc)
sys.path.remove(root)
candidate = None candidate = None
for _, obj in inspect.getmembers(tmp): for _, obj in inspect.getmembers(tmp):
if inspect.isclass(obj) and inspect.getmodule(obj) == tmp: if inspect.isclass(obj) and inspect.getmodule(obj) == tmp:
@@ -404,22 +360,6 @@ class Senpy(object):
def filter_plugins(self, **kwargs): def filter_plugins(self, **kwargs):
""" Filter plugins by different criteria """ """ Filter plugins by different criteria """
ptype = kwargs.pop('plugin_type', None)
logger.debug('#' * 100)
logger.debug('ptype {}'.format(ptype))
if ptype:
try:
ptype = ptype[0].upper() + ptype[1:]
pclass = getattr(plugins, ptype)
logger.debug('Class: {}'.format(pclass))
candidates = filter(lambda x: isinstance(x, pclass),
self.plugins.values())
except AttributeError:
raise Error('{} is not a valid type'.format(ptype))
else:
candidates = self.plugins.values()
logger.debug(candidates)
def matches(plug): def matches(plug):
res = all(getattr(plug, k, None) == v for (k, v) in kwargs.items()) res = all(getattr(plug, k, None) == v for (k, v) in kwargs.items())
@@ -427,11 +367,15 @@ class Senpy(object):
"matching {} with {}: {}".format(plug.name, kwargs, res)) "matching {} with {}: {}".format(plug.name, kwargs, res))
return res return res
if kwargs: if not kwargs:
candidates = filter(matches, candidates) return self.plugins
return {p.name: p for p in candidates} else:
return {n: p for n, p in self.plugins.items() if matches(p)}
@property def sentiment_plugins(self):
def analysis_plugins(self): """ Return only the sentiment plugins """
""" Return only the analysis plugins """ return {
return self.filter_plugins(plugin_type='analysisPlugin') p: plugin
for p, plugin in self.plugins.items()
if isinstance(plugin, SentimentPlugin)
}

View File

@@ -3,17 +3,16 @@ standard_library.install_aliases()
import inspect import inspect
import os.path import os.path
import os
import pickle import pickle
import logging import logging
import tempfile import tempfile
import copy import copy
from .. import models from . import models
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Plugin(models.Plugin): class SenpyPlugin(models.Plugin):
def __init__(self, info=None): def __init__(self, info=None):
""" """
Provides a canonical name for plugins and serves as base for other Provides a canonical name for plugins and serves as base for other
@@ -24,24 +23,12 @@ class Plugin(models.Plugin):
"information for the plugin.")) "information for the plugin."))
logger.debug("Initialising {}".format(info)) logger.debug("Initialising {}".format(info))
id = 'plugins/{}_{}'.format(info['name'], info['version']) id = 'plugins/{}_{}'.format(info['name'], info['version'])
super(Plugin, self).__init__(id=id, **info) super(SenpyPlugin, self).__init__(id=id, **info)
self.is_activated = False self.is_activated = False
def get_folder(self): def get_folder(self):
return os.path.dirname(inspect.getfile(self.__class__)) return os.path.dirname(inspect.getfile(self.__class__))
def activate(self):
pass
def deactivate(self):
pass
SenpyPlugin = Plugin
class AnalysisPlugin(Plugin):
def analyse(self, *args, **kwargs): def analyse(self, *args, **kwargs):
raise NotImplemented( raise NotImplemented(
'Your method should implement either analyse or analyse_entry') 'Your method should implement either analyse or analyse_entry')
@@ -60,33 +47,30 @@ class AnalysisPlugin(Plugin):
for i in results.entries: for i in results.entries:
yield i yield i
def analyse_entries(self, entries, parameters): def activate(self):
for entry in entries: pass
logger.debug('Analysing entry with plugin {}: {}'.format(self, entry))
for result in self.analyse_entry(entry, parameters): def deactivate(self):
yield result pass
class ConversionPlugin(Plugin): class SentimentPlugin(models.SentimentPlugin, SenpyPlugin):
pass
class SentimentPlugin(models.SentimentPlugin, AnalysisPlugin):
def __init__(self, info, *args, **kwargs): def __init__(self, info, *args, **kwargs):
super(SentimentPlugin, self).__init__(info, *args, **kwargs) super(SentimentPlugin, self).__init__(info, *args, **kwargs)
self.minPolarityValue = float(info.get("minPolarityValue", 0)) self.minPolarityValue = float(info.get("minPolarityValue", 0))
self.maxPolarityValue = float(info.get("maxPolarityValue", 1)) self.maxPolarityValue = float(info.get("maxPolarityValue", 1))
class EmotionPlugin(models.EmotionPlugin, AnalysisPlugin): class EmotionPlugin(models.EmotionPlugin, SenpyPlugin):
def __init__(self, info, *args, **kwargs): def __init__(self, info, *args, **kwargs):
super(EmotionPlugin, self).__init__(info, *args, **kwargs) super(EmotionPlugin, self).__init__(info, *args, **kwargs)
self.minEmotionValue = float(info.get("minEmotionValue", -1)) self.minEmotionValue = float(info.get("minEmotionValue", -1))
self.maxEmotionValue = float(info.get("maxEmotionValue", 1)) self.maxEmotionValue = float(info.get("maxEmotionValue", 1))
class EmotionConversionPlugin(models.EmotionConversionPlugin, ConversionPlugin): class EmotionConversionPlugin(models.EmotionConversionPlugin, SenpyPlugin):
pass def __init__(self, info, *args, **kwargs):
super(EmotionConversionPlugin, self).__init__(info, *args, **kwargs)
class ShelfMixin(object): class ShelfMixin(object):
@@ -108,8 +92,8 @@ class ShelfMixin(object):
@property @property
def shelf_file(self): def shelf_file(self):
if 'shelf_file' not in self or not self['shelf_file']: if 'shelf_file' not in self or not self['shelf_file']:
sd = os.environ.get('SENPY_DATA', tempfile.gettempdir()) self.shelf_file = os.path.join(tempfile.gettempdir(),
self.shelf_file = os.path.join(sd, self.name + '.p') self.name + '.p')
return self['shelf_file'] return self['shelf_file']
def save(self): def save(self):

View File

@@ -1,80 +0,0 @@
from senpy.plugins import EmotionConversionPlugin
from senpy.models import EmotionSet, Emotion, Error
import logging
logger = logging.getLogger(__name__)
class CentroidConversion(EmotionConversionPlugin):
def __init__(self, info):
if 'centroids' not in info:
raise Error('Centroid conversion plugins should provide '
'the centroids in their senpy file')
if 'onyx:doesConversion' not in info:
if 'centroids_direction' not in info:
raise Error('Please, provide centroids direction')
cf, ct = info['centroids_direction']
info['onyx:doesConversion'] = [{
'onyx:conversionFrom': cf,
'onyx:conversionTo': ct
}, {
'onyx:conversionFrom': ct,
'onyx:conversionTo': cf
}]
if 'aliases' in info:
aliases = info['aliases']
ncentroids = {}
for k1, v1 in info['centroids'].items():
nv1 = {}
for k2, v2 in v1.items():
nv1[aliases.get(k2, k2)] = v2
ncentroids[aliases.get(k1, k1)] = nv1
info['centroids'] = ncentroids
super(CentroidConversion, self).__init__(info)
def _forward_conversion(self, original):
"""Sum the VAD value of all categories found."""
res = Emotion()
for e in original.onyx__hasEmotion:
category = e.onyx__hasEmotionCategory
if category in self.centroids:
for dim, value in self.centroids[category].items():
try:
res[dim] += value
except Exception:
res[dim] = value
return res
def _backwards_conversion(self, original):
"""Find the closest category"""
dimensions = list(self.centroids.values())[0]
def distance(e1, e2):
return sum((e1[k] - e2.get(k, 0)) for k in dimensions)
emotion = ''
mindistance = 10000000000000000000000.0
for state in self.centroids:
d = distance(self.centroids[state], original)
if d < mindistance:
mindistance = d
emotion = state
result = Emotion(onyx__hasEmotionCategory=emotion)
return result
def convert(self, emotionSet, fromModel, toModel, params):
cf, ct = self.centroids_direction
logger.debug(
'{}\n{}\n{}\n{}'.format(emotionSet, fromModel, toModel, params))
e = EmotionSet()
if fromModel == cf and toModel == ct:
e.onyx__hasEmotion.append(self._forward_conversion(emotionSet))
elif fromModel == ct and toModel == cf:
for i in emotionSet.onyx__hasEmotion:
e.onyx__hasEmotion.append(self._backwards_conversion(i))
else:
raise Error('EMOTION MODEL NOT KNOWN')
yield e

View File

@@ -1,39 +0,0 @@
---
name: Ekman2FSRE
module: senpy.plugins.conversion.centroids
description: Plugin to convert emotion sets from Ekman to VAD
version: 0.1
# No need to specify onyx:doesConversion because centroids.py adds it automatically from centroids_direction
centroids:
anger:
A: 6.95
D: 5.1
V: 2.7
disgust:
A: 5.3
D: 8.05
V: 2.7
fear:
A: 6.5
D: 3.6
V: 3.2
happiness:
A: 7.22
D: 6.28
V: 8.6
sadness:
A: 5.21
D: 2.82
V: 2.21
centroids_direction:
- emoml:big6
- emoml:fsre-dimensions
aliases: # These are aliases for any key in the centroid, to avoid repeating a long name several times
A: emoml:arousal
V: emoml:valence
D: emoml:dominance
anger: emoml:big6anger
disgust: emoml:big6disgust
fear: emoml:big6fear
happiness: emoml:big6happiness
sadness: emoml:big6sadness

View File

@@ -0,0 +1,56 @@
from senpy.plugins import EmotionConversionPlugin
from senpy.models import EmotionSet, Emotion, Error
import logging
logger = logging.getLogger(__name__)
import math
class WNA2VAD(EmotionConversionPlugin):
def _ekman_to_vad(self, ekmanSet):
potency = 0
arousal = 0
dominance = 0
for e in ekmanSet.onyx__hasEmotion:
category = e.onyx__hasEmotionCategory
centroid = self.centroids[category]
potency += centroid['V']
arousal += centroid['A']
dominance += centroid['D']
e = Emotion({'emoml:potency': potency,
'emoml:arousal': arousal,
'emoml:dominance': dominance})
return e
def _vad_to_ekman(self, VADEmotion):
V = VADEmotion['emoml:valence']
A = VADEmotion['emoml:potency']
D = VADEmotion['emoml:dominance']
emotion = ''
value = 10000000000000000000000.0
for state in self.centroids:
valence = V - self.centroids[state]['V']
arousal = A - self.centroids[state]['A']
dominance = D - self.centroids[state]['D']
new_value = math.sqrt((valence**2) +
(arousal**2) +
(dominance**2))
if new_value < value:
value = new_value
emotion = state
result = Emotion(onyx__hasEmotionCategory=emotion)
return result
def convert(self, emotionSet, fromModel, toModel, params):
logger.debug('{}\n{}\n{}\n{}'.format(emotionSet, fromModel, toModel, params))
e = EmotionSet()
if fromModel == 'emoml:big6':
e.onyx__hasEmotion.append(self._ekman_to_vad(emotionSet))
elif fromModel == 'emoml:fsre-dimensions':
for i in emotionSet.onyx__hasEmotion:
e.onyx__hasEmotion.append(self._vad_to_ekman(e))
else:
raise Error('EMOTION MODEL NOT KNOWN')
yield e

View File

@@ -1,39 +1,35 @@
--- ---
name: Ekman2PAD name: Ekman2VAD
module: senpy.plugins.conversion.centroids module: ekman2vad
description: Plugin to convert emotion sets from Ekman to VAD description: Plugin to convert from Ekman to VAD
version: 0.1 version: 0.1
# No need to specify onyx:doesConversion because centroids.py adds it automatically from centroids_direction onyx:doesConversion:
- onyx:conversionFrom: emoml:big6
onyx:conversionTo: emoml:fsre-dimensions
- onyx:conversionFrom: emoml:fsre-dimensions
onyx:conversionTo: wna:WNAModel
centroids: centroids:
anger: emoml:big6anger:
A: 6.95 A: 6.95
D: 5.1 D: 5.1
V: 2.7 V: 2.7
disgust: emoml:big6disgust:
A: 5.3 A: 5.3
D: 8.05 D: 8.05
V: 2.7 V: 2.7
fear: emoml:big6fear:
A: 6.5 A: 6.5
D: 3.6 D: 3.6
V: 3.2 V: 3.2
happiness: emoml:big6happiness:
A: 7.22 A: 7.22
D: 6.28 D: 6.28
V: 8.6 V: 8.6
sadness: emoml:big6sadness:
A: 5.21 A: 5.21
D: 2.82 D: 2.82
V: 2.21 V: 2.21
centroids_direction: aliases:
- emoml:big6
- emoml:pad
aliases: # These are aliases for any key in the centroid, to avoid repeating a long name several times
A: emoml:arousal A: emoml:arousal
V: emoml:valence V: emoml:potency
D: emoml:dominance D: emoml:dominance
anger: emoml:big6anger
disgust: emoml:big6disgust
fear: emoml:big6fear
happiness: emoml:big6happiness
sadness: emoml:big6sadness

View File

@@ -37,12 +37,6 @@
"@type": "@id", "@type": "@id",
"@container": "@set" "@container": "@set"
}, },
"plugins": {
"@container": "@list"
},
"options": {
"@container": "@set"
},
"prov:wasGeneratedBy": { "prov:wasGeneratedBy": {
"@type": "@id" "@type": "@id"
}, },

View File

@@ -6,7 +6,6 @@
"properties": { "properties": {
"plugins": { "plugins": {
"type": "array", "type": "array",
"default": [],
"items": { "items": {
"$ref": "plugin.json" "$ref": "plugin.json"
} }

View File

@@ -8,12 +8,8 @@ DEFAULT_FILE = os.path.join(ROOT, 'VERSION')
def read_version(versionfile=DEFAULT_FILE): def read_version(versionfile=DEFAULT_FILE):
try: with open(versionfile) as f:
with open(versionfile) as f: return f.read().strip()
return f.read().strip()
except IOError:
logger.error('Running an unknown version of senpy. Be careful!.')
return '0.0'
__version__ = read_version() __version__ = read_version()

View File

@@ -9,6 +9,4 @@ test=pytest
ignore = E402 ignore = E402
max-line-length = 100 max-line-length = 100
[bdist_wheel] [bdist_wheel]
universal=1 universal=1
[tool:pytest]
addopts = --cov=senpy --cov-report term-missing

View File

@@ -1,3 +1,3 @@
pytest
mock mock
pytest-cov pytest-cov
pytest

View File

@@ -4,5 +4,4 @@ from senpy.plugins import SentimentPlugin
class DummyPlugin(SentimentPlugin): class DummyPlugin(SentimentPlugin):
def analyse_entry(self, entry, params): def analyse_entry(self, entry, params):
entry.text = entry.text[::-1] entry.text = entry.text[::-1]
entry.reversed = entry.get('reversed', 0) + 1
yield entry yield entry

View File

@@ -1,8 +1,8 @@
from senpy.plugins import AnalysisPlugin from senpy.plugins import SenpyPlugin
from time import sleep from time import sleep
class SleepPlugin(AnalysisPlugin): class SleepPlugin(SenpyPlugin):
def activate(self, *args, **kwargs): def activate(self, *args, **kwargs):
sleep(self.timeout) sleep(self.timeout)

View File

@@ -1,68 +0,0 @@
import logging
logger = logging.getLogger(__name__)
from unittest import TestCase
from senpy.api import parse_params, API_PARAMS, NIF_PARAMS, WEB_PARAMS
from senpy.models import Error
class APITest(TestCase):
def test_api_params(self):
"""The API should not define any required parameters without a default"""
parse_params({}, spec=API_PARAMS)
def test_web_params(self):
"""The WEB should not define any required parameters without a default"""
parse_params({}, spec=WEB_PARAMS)
def test_basic(self):
a = {}
try:
parse_params(a, spec=NIF_PARAMS)
raise AssertionError()
except Error:
pass
a = {'input': 'hello'}
p = parse_params(a, spec=NIF_PARAMS)
assert 'input' in p
b = {'i': 'hello'}
p = parse_params(b, spec=NIF_PARAMS)
assert 'input' in p
def test_plugin(self):
query = {}
plug_params = {
'hello': {
'aliases': ['hello', 'hiya'],
'required': True
}
}
try:
parse_params(query, spec=plug_params)
raise AssertionError()
except Error:
pass
query['hello'] = 'world'
p = parse_params(query, spec=plug_params)
assert 'hello' in p
assert p['hello'] == 'world'
del query['hello']
query['hiya'] = 'dlrow'
p = parse_params(query, spec=plug_params)
assert 'hello' in p
assert 'hiya' in p
assert p['hello'] == 'dlrow'
def test_default(self):
spec = {
'hello': {
'required': True,
'default': 1
}
}
p = parse_params({}, spec=spec)
assert 'hello' in p
assert p['hello'] == 1

View File

@@ -34,11 +34,8 @@ class ModelsTest(TestCase):
url=endpoint + '/', method='GET', params={'input': 'hello'}) url=endpoint + '/', method='GET', params={'input': 'hello'})
error = Call(Error('Nothing')) error = Call(Error('Nothing'))
with patch('requests.request', return_value=error) as patched: with patch('requests.request', return_value=error) as patched:
try: resp = client.analyse(input='hello', algorithm='NONEXISTENT')
client.analyse(input='hello', algorithm='NONEXISTENT') assert isinstance(resp, Error)
raise Exception('Exceptions should be raised. This is not golang')
except Error:
pass
patched.assert_called_with( patched.assert_called_with(
url=endpoint + '/', url=endpoint + '/',
method='GET', method='GET',

View File

@@ -1,6 +1,5 @@
from __future__ import print_function from __future__ import print_function
import os import os
from copy import deepcopy
import logging import logging
try: try:
@@ -10,7 +9,7 @@ except ImportError:
from functools import partial from functools import partial
from senpy.extensions import Senpy from senpy.extensions import Senpy
from senpy.models import Error, Results, Entry, EmotionSet, Emotion, Plugin from senpy.models import Error
from flask import Flask from flask import Flask
from unittest import TestCase from unittest import TestCase
@@ -43,7 +42,6 @@ class ExtensionsTest(TestCase):
info = { info = {
'name': 'TestPip', 'name': 'TestPip',
'module': 'dummy', 'module': 'dummy',
'description': None,
'requirements': ['noop'], 'requirements': ['noop'],
'version': 0 'version': 0
} }
@@ -53,7 +51,6 @@ class ExtensionsTest(TestCase):
assert module assert module
import noop import noop
dir(noop) dir(noop)
self.senpy.install_deps()
def test_installing(self): def test_installing(self):
""" Enabling a plugin """ """ Enabling a plugin """
@@ -98,26 +95,17 @@ class ExtensionsTest(TestCase):
def test_analyse_error(self): def test_analyse_error(self):
mm = mock.MagicMock() mm = mock.MagicMock()
mm.id = 'magic_mock' mm.analyse_entry.side_effect = Error('error on analysis', status=900)
mm.analyse_entries.side_effect = Error('error on analysis', status=500)
self.senpy.plugins['MOCK'] = mm self.senpy.plugins['MOCK'] = mm
try: resp = self.senpy.analyse(input='nothing', algorithm='MOCK')
self.senpy.analyse(input='nothing', algorithm='MOCK') assert resp['message'] == 'error on analysis'
assert False assert resp['status'] == 900
except Error as ex:
assert ex['message'] == 'error on analysis'
assert ex['status'] == 500
mm.analyse.side_effect = Exception('generic exception on analysis') mm.analyse.side_effect = Exception('generic exception on analysis')
mm.analyse_entries.side_effect = Exception( mm.analyse_entry.side_effect = Exception(
'generic exception on analysis') 'generic exception on analysis')
resp = self.senpy.analyse(input='nothing', algorithm='MOCK')
try: assert resp['message'] == 'generic exception on analysis'
self.senpy.analyse(input='nothing', algorithm='MOCK') assert resp['status'] == 500
assert False
except Error as ex:
assert ex['message'] == 'generic exception on analysis'
assert ex['status'] == 500
def test_filtering(self): def test_filtering(self):
""" Filtering plugins """ """ Filtering plugins """
@@ -131,43 +119,3 @@ class ExtensionsTest(TestCase):
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.dir, default_plugins=True)
assert len(senpy.plugins) > 1 assert len(senpy.plugins) > 1
def test_convert_emotions(self):
self.senpy.activate_all()
plugin = Plugin({
'id': 'imaginary',
'onyx:usesEmotionModel': 'emoml:fsre-dimensions'
})
eSet1 = EmotionSet()
eSet1.prov__wasGeneratedBy = plugin['id']
eSet1['onyx:hasEmotion'].append(Emotion({
'emoml:arousal': 1,
'emoml:potency': 0,
'emoml:valence': 0
}))
response = Results({
'entries': [Entry({
'text': 'much ado about nothing',
'emotions': [eSet1]
})]
})
params = {'emotionModel': 'emoml:big6',
'conversion': 'full'}
r1 = deepcopy(response)
self.senpy.convert_emotions(r1,
[plugin, ],
params)
assert len(r1.entries[0].emotions) == 2
params['conversion'] = 'nested'
r2 = deepcopy(response)
self.senpy.convert_emotions(r2,
[plugin, ],
params)
assert len(r2.entries[0].emotions) == 1
assert r2.entries[0].emotions[0]['prov:wasDerivedFrom'] == eSet1
params['conversion'] = 'filtered'
r3 = deepcopy(response)
self.senpy.convert_emotions(r3,
[plugin, ],
params)
assert len(r3.entries[0].emotions) == 1

View File

@@ -11,10 +11,8 @@ from senpy.models import (Emotion,
Entry, Entry,
Error, Error,
Results, Results,
Sentiment, Sentiment)
Plugins, from senpy.plugins import SenpyPlugin
Plugin)
from senpy import plugins
from pprint import pprint from pprint import pprint
@@ -55,8 +53,8 @@ class ModelsTest(TestCase):
assert (received["entries"][0]["nif:isString"] != "Not testing") assert (received["entries"][0]["nif:isString"] != "Not testing")
def test_id(self): def test_id(self):
""" Adding the id after creation should overwrite the automatic ID ''' Adding the id after creation should overwrite the automatic ID
""" '''
r = Entry() r = Entry()
j = r.jsonld() j = r.jsonld()
assert '@id' in j assert '@id' in j
@@ -96,16 +94,8 @@ class ModelsTest(TestCase):
r.validate() r.validate()
def test_plugins(self): def test_plugins(self):
self.assertRaises(Error, plugins.Plugin) self.assertRaises(Error, SenpyPlugin)
p = plugins.Plugin({"name": "dummy", p = SenpyPlugin({"name": "dummy", "version": 0})
"version": 0,
"extra_params": {
"none": {
"options": ["es", ],
"required": False,
"default": "0"
}
}})
c = p.jsonld() c = p.jsonld()
assert "info" not in c assert "info" not in c
assert "repo" not in c assert "repo" not in c
@@ -113,13 +103,11 @@ class ModelsTest(TestCase):
logging.debug("Framed:") logging.debug("Framed:")
logging.debug(c) logging.debug(c)
p.validate() p.validate()
assert "es" in c['extra_params']['none']['options']
assert isinstance(c['extra_params']['none']['options'], list)
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 = SenpyPlugin({"name": "STR test", "version": 0})
p._testing = 0 p._testing = 0
s = str(p) s = str(p)
assert "_testing" not in s assert "_testing" not in s
@@ -156,14 +144,6 @@ class ModelsTest(TestCase):
g = rdflib.Graph().parse(data=t, format='turtle') g = rdflib.Graph().parse(data=t, format='turtle')
assert len(g) == len(triples) assert len(g) == len(triples)
def test_single_plugin(self): def test_convert_emotions(self):
"""A response with a single plugin should still return a list""" """It should be possible to convert between different emotion models"""
plugs = Plugins() pass
for i in range(10):
p = Plugin({'id': str(i),
'version': 0,
'description': 'dummy'})
plugs.plugins.append(p)
assert isinstance(plugs.plugins, list)
js = plugs.jsonld()
assert isinstance(js['plugins'], list)

2
version.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
git describe --long --tags --dirty