mirror of
https://github.com/gsi-upm/senpy
synced 2025-10-20 10:18:26 +00:00
Compare commits
1 Commits
0.8.5
...
0.7.1-5-g8
Author | SHA1 | Date | |
---|---|---|---|
|
87589e994a |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,5 +6,3 @@ build
|
|||||||
README.html
|
README.html
|
||||||
__pycache__
|
__pycache__
|
||||||
VERSION
|
VERSION
|
||||||
Dockerfile-*
|
|
||||||
Dockerfile
|
|
@@ -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
|
|
@@ -1,5 +0,0 @@
|
|||||||
- repo: git://github.com/pre-commit/pre-commit-hooks
|
|
||||||
sha: e626cd57090d8df0be21e4df0f4e55cc3511d6ab
|
|
||||||
hooks:
|
|
||||||
- id: flake8
|
|
||||||
- id: check-json
|
|
17
.travis.yml
17
.travis.yml
@@ -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
1
Dockerfile
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
Dockerfile-3.5
|
21
Dockerfile-2.7
Normal file
21
Dockerfile-2.7
Normal 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
21
Dockerfile-3.4
Normal 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
21
Dockerfile-3.5
Normal 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
33
Dockerfile.deps
Normal 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"]
|
@@ -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/
|
|
||||||
|
|
||||||
|
|
||||||
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"]
|
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]
|
75
Makefile
75
Makefile
@@ -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
|
|
||||||
|
55
README.rst
55
README.rst
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
34
docs/api.rst
34
docs/api.rst
@@ -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
|
||||||
|
|
||||||
@@ -96,9 +94,7 @@ NIF API
|
|||||||
"@context": {
|
"@context": {
|
||||||
...
|
...
|
||||||
},
|
},
|
||||||
"@type": "plugins",
|
"sentiment140": {
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "sentiment140",
|
"name": "sentiment140",
|
||||||
"is_activated": true,
|
"is_activated": true,
|
||||||
"version": "0.1",
|
"version": "0.1",
|
||||||
@@ -119,7 +115,8 @@ NIF API
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@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",
|
||||||
@@ -141,7 +138,6 @@ NIF API
|
|||||||
},
|
},
|
||||||
"@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"
|
||||||
|
}
|
||||||
|
@@ -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
|
|
@@ -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
|
||||||
|
@@ -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.
|
||||||
|
@@ -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`
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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",
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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
|
|
||||||
|
@@ -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,25 +76,18 @@ 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)
|
|
||||||
algos = None
|
|
||||||
if "algorithm" in api_params and api_params["algorithm"]:
|
|
||||||
algos = api_params["algorithm"].split(',')
|
|
||||||
elif self.default_plugin:
|
|
||||||
algos = [self.default_plugin.name, ]
|
|
||||||
else:
|
|
||||||
raise Error(
|
|
||||||
status=404,
|
|
||||||
message="No default plugin found, and None provided")
|
|
||||||
|
|
||||||
plugins = list()
|
|
||||||
for algo in algos:
|
|
||||||
if algo not in self.plugins:
|
if algo not in self.plugins:
|
||||||
logger.debug(("The algorithm '{}' is not valid\n"
|
logger.debug(("The algorithm '{}' is not valid\n"
|
||||||
"Valid algorithms: {}").format(algo,
|
"Valid algorithms: {}").format(algo,
|
||||||
@@ -111,68 +102,44 @@ class Senpy(object):
|
|||||||
status=400,
|
status=400,
|
||||||
message=("The algorithm '{}'"
|
message=("The algorithm '{}'"
|
||||||
" is not activated yet").format(algo))
|
" is not activated yet").format(algo))
|
||||||
plugins.append(self.plugins[algo])
|
return self.plugins[algo]
|
||||||
return plugins
|
|
||||||
|
|
||||||
def _get_params(self, params, plugin=None):
|
def _get_params(self, params, plugin):
|
||||||
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,18 +161,16 @@ 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
|
||||||
|
|
||||||
logger.debug('Asked for model: {}'.format(toModel))
|
|
||||||
output = params.get('conversion', None)
|
|
||||||
candidates = {}
|
|
||||||
for plugin in plugins:
|
|
||||||
try:
|
try:
|
||||||
fromModel = plugin.get('onyx:usesEmotionModel', None)
|
candidate = next(self._conversion_candidates(fromModel, toModel))
|
||||||
candidates[plugin.id] = next(self._conversion_candidates(fromModel, toModel))
|
|
||||||
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)))
|
||||||
@@ -213,16 +178,12 @@ class Senpy(object):
|
|||||||
e.parameters = params
|
e.parameters = params
|
||||||
raise e
|
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)
|
||||||
|
}
|
||||||
|
@@ -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):
|
|
||||||
yield result
|
|
||||||
|
|
||||||
|
def deactivate(self):
|
||||||
class ConversionPlugin(Plugin):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SentimentPlugin(models.SentimentPlugin, AnalysisPlugin):
|
class SentimentPlugin(models.SentimentPlugin, SenpyPlugin):
|
||||||
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):
|
@@ -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
|
|
@@ -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
|
|
56
senpy/plugins/conversion/emotion/ekman2vad.py
Normal file
56
senpy/plugins/conversion/emotion/ekman2vad.py
Normal 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
|
@@ -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
|
|
@@ -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"
|
||||||
},
|
},
|
||||||
|
@@ -6,7 +6,6 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"default": [],
|
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "plugin.json"
|
"$ref": "plugin.json"
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
||||||
|
@@ -10,5 +10,3 @@ 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
|
|
@@ -1,3 +1,3 @@
|
|||||||
|
pytest
|
||||||
mock
|
mock
|
||||||
pytest-cov
|
pytest-cov
|
||||||
pytest
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
|
|
||||||
|
@@ -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
|
|
@@ -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',
|
||||||
|
@@ -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
|
|
||||||
|
@@ -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
2
version.sh
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
git describe --long --tags --dirty
|
Reference in New Issue
Block a user