1
0
mirror of https://github.com/gsi-upm/senpy synced 2025-10-19 17:58:28 +00:00

Compare commits

..

2 Commits

Author SHA1 Message Date
militarpancho
83e2d415a1 Change name to split, according to issue #37 2017-06-13 19:44:40 +02:00
militarpancho
f8ca595bc9 Added chunker plugin to tokenize texts 2017-06-13 14:00:40 +02:00
102 changed files with 1438 additions and 4362 deletions

View File

@@ -12,7 +12,7 @@ stages:
- clean - clean
before_script: before_script:
- make -e login - docker login -u $HUB_USER -p $HUB_PASSWORD
.test: &test_definition .test: &test_definition
stage: test stage: test
@@ -36,7 +36,6 @@ test-2.7:
only: only:
- tags - tags
- triggers - triggers
- fix-makefiles
push-3.5: push-3.5:
<<: *image_definition <<: *image_definition
@@ -55,7 +54,6 @@ push-latest:
only: only:
- master - master
- triggers - triggers
- fix-makefiles
push-github: push-github:
stage: deploy stage: deploy
@@ -64,13 +62,11 @@ push-github:
only: only:
- master - master
- triggers - triggers
- fix-makefiles
deploy_pypi: deploy_pypi:
stage: deploy stage: deploy
script: # Configure the PyPI credentials, then push the package, and cleanup the creds. script: # Configure the PyPI credentials, then push the package, and cleanup the creds.
- echo "[server-login]" >> ~/.pypirc - echo "[server-login]" >> ~/.pypirc
- echo "repository=https://upload.pypi.org/legacy/" >> ~/.pypirc
- echo "username=" ${PYPI_USER} >> ~/.pypirc - echo "username=" ${PYPI_USER} >> ~/.pypirc
- echo "password=" ${PYPI_PASSWORD} >> ~/.pypirc - echo "password=" ${PYPI_PASSWORD} >> ~/.pypirc
- make pip_upload - make pip_upload
@@ -87,7 +83,6 @@ deploy:
- make -e deploy - make -e deploy
only: only:
- master - master
- fix-makefiles
push-github: push-github:
stage: deploy stage: deploy

View File

@@ -1,27 +0,0 @@
These makefiles are recipes for several common tasks in different types of projects.
To add them to your project, simply do:
```
git remote add makefiles ssh://git@lab.cluster.gsi.dit.upm.es:2200/docs/templates/makefiles.git
git subtree add --prefix=.makefiles/ makefiles master
touch Makefile
echo "include .makefiles/base.mk" >> Makefile
```
Now you can take advantage of the recipes.
For instance, to add useful targets for a python project, just add this to your Makefile:
```
include .makefiles/python.mk
```
You may need to set special variables like the name of your project or the python versions you're targetting.
Take a look at each specific `.mk` file for more information, and the `Makefile` in the [senpy](https://lab.cluster.gsi.dit.upm.es/senpy/senpy) project for a real use case.
If you update the makefiles from your repository, make sure to push the changes for review in upstream (this repository):
```
make makefiles-push
```
It will automatically commit all unstaged changes in the .makefiles folder.

View File

@@ -1,36 +0,0 @@
export
NAME ?= $(shell basename $(CURDIR))
VERSION ?= $(shell git describe --tags --dirty 2>/dev/null)
ifeq ($(VERSION),)
VERSION:=unknown
endif
# Get the location of this makefile.
MK_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
-include .env
-include ../.env
help: ## Show this help.
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/\(.*:\)[^#]*##\s*\(.*\)/\1\t\2/' | column -t -s " "
config: ## Load config from the environment. You should run it once in every session before other tasks. Run: eval $(make config)
@awk '{ print "export " $$0}' ../.env
@awk '{ print "export " $$0}' .env
@echo "# Please, run: "
@echo "# eval \$$(make config)"
# If you need to run a command on the key/value pairs, use this:
# @awk '{ split($$0, a, "="); "echo " a[2] " | base64 -w 0" |& getline b64; print "export " a[1] "=" a[2]; print "export " a[1] "_BASE64=" b64}' .env
ci: ## Run a task using gitlab-runner. Only use to debug problems in the CI pipeline
gitlab-runner exec shell --builds-dir '.builds' --env CI_PROJECT_NAME=$(NAME) ${action}
include $(MK_DIR)/makefiles.mk
include $(MK_DIR)/docker.mk
include $(MK_DIR)/git.mk
info:: ## List all variables
env
.PHONY:: config help ci

View File

@@ -1,29 +0,0 @@
IMAGENAME?=$(NAME)
IMAGEWTAG?=$(IMAGENAME):$(VERSION)
docker-login: ## Log in to the registry. It will only be used in the server, or when running a CI task locally (if CI_BUILD_TOKEN is set).
ifeq ($(CI_BUILD_TOKEN),)
@echo "Not logging in to the docker registry" "$(CI_REGISTRY)"
else
@docker login -u gitlab-ci-token -p $(CI_BUILD_TOKEN) $(CI_REGISTRY)
endif
ifeq ($(HUB_USER),)
@echo "Not logging in to global the docker registry"
else
@docker login -u $(HUB_USER) -p $(HUB_PASSWORD)
endif
docker-clean: ## Remove docker credentials
ifeq ($(HUB_USER),)
else
@docker logout
endif
login:: docker-login
clean:: docker-clean
docker-info:
@echo IMAGEWTAG=${IMAGEWTAG}
.PHONY:: docker-login docker-clean login clean

View File

@@ -1,28 +0,0 @@
commit:
git commit -a
tag:
git tag ${VERSION}
git-push::
git push --tags -u origin HEAD
git-pull:
git pull --all
push-github: ## Push the code to github. You need to set up GITHUB_DEPLOY_KEY
ifeq ($(GITHUB_DEPLOY_KEY),)
else
$(eval KEY_FILE := "$(shell mktemp)")
@echo "$(GITHUB_DEPLOY_KEY)" > $(KEY_FILE)
@git remote rm github-deploy || true
git remote add github-deploy $(GITHUB_REPO)
-@GIT_SSH_COMMAND="ssh -i $(KEY_FILE)" git fetch github-deploy $(CI_COMMIT_REF_NAME)
@GIT_SSH_COMMAND="ssh -i $(KEY_FILE)" git push github-deploy HEAD:$(CI_COMMIT_REF_NAME)
rm $(KEY_FILE)
endif
push:: git-push
pull:: git-pull
.PHONY:: commit tag push git-push git-pull push-github

View File

@@ -1,51 +0,0 @@
# Deployment with Kubernetes
# KUBE_CA_PEM_FILE is the path of a certificate file. It automatically set by GitLab
# if you enable Kubernetes integration in a project.
#
# As of this writing, Kubernetes integration can not be set on a group level, so it has to
# be manually set in every project.
# Alternatively, we use a custom KUBE_CA_BUNDLE environment variable, which can be set at
# the group level. In this case, the variable contains the whole content of the certificate,
# which we dump to a temporary file
#
# Check if the KUBE_CA_PEM_FILE exists. Otherwise, create it from KUBE_CA_BUNDLE
KUBE_CA_TEMP=false
ifndef KUBE_CA_PEM_FILE
KUBE_CA_PEM_FILE:=$$PWD/.ca.crt
CREATED:=$(shell echo -e "$(KUBE_CA_BUNDLE)" > $(KUBE_CA_PEM_FILE))
endif
KUBE_TOKEN?=""
KUBE_NAMESPACE?=$(NAME)
KUBECTL=docker run --rm -v $(KUBE_CA_PEM_FILE):/tmp/ca.pem -i lachlanevenson/k8s-kubectl --server="$(KUBE_URL)" --token="$(KUBE_TOKEN)" --certificate-authority="/tmp/ca.pem" -n $(KUBE_NAMESPACE)
CI_COMMIT_REF_NAME?=master
info:: ## Print variables. Useful for debugging.
@echo "#KUBERNETES"
@echo KUBE_URL=$(KUBE_URL)
@echo KUBE_CA_PEM_FILE=$(KUBE_CA_PEM_FILE)
@echo KUBE_CA_BUNDLE=$$KUBE_CA_BUNDLE
@echo KUBE_TOKEN=$(KUBE_TOKEN)
@echo KUBE_NAMESPACE=$(KUBE_NAMESPACE)
@echo KUBECTL=$(KUBECTL)
@echo "#CI"
@echo CI_PROJECT_NAME=$(CI_PROJECT_NAME)
@echo CI_REGISTRY=$(CI_REGISTRY)
@echo CI_REGISTRY_USER=$(CI_REGISTRY_USER)
@echo CI_COMMIT_REF_NAME=$(CI_COMMIT_REF_NAME)
@echo "CREATED=$(CREATED)"
#
# Deployment and advanced features
#
deploy: ## Deploy to kubernetes using the credentials in KUBE_CA_PEM_FILE (or KUBE_CA_BUNDLE ) and TOKEN
@ls k8s/*.yaml k8s/*.yml k8s/*.tmpl 2>/dev/null || true
@cat k8s/*.yaml k8s/*.yml k8s/*.tmpl 2>/dev/null | envsubst | $(KUBECTL) apply -f -
deploy-check: ## Get the deployed configuration.
@$(KUBECTL) get deploy,pods,svc,ingress
.PHONY:: info deploy deploy-check

View File

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

View File

@@ -1,5 +0,0 @@
init: ## Init pre-commit hooks (i.e. enforcing format checking before allowing a commit)
pip install --user pre-commit
pre-commit install
.PHONY:: init

View File

@@ -1,99 +0,0 @@
PYVERSIONS ?= 3.5
PYMAIN ?= $(firstword $(PYVERSIONS))
TARNAME ?= $(NAME)-$(VERSION).tar.gz
VERSIONFILE ?= $(NAME)/VERSION
DEVPORT ?= 6000
.FORCE:
version: .FORCE
@echo $(VERSION) > $(VERSIONFILE)
@echo $(VERSION)
yapf: ## Format python code
yapf -i -r $(NAME)
yapf -i -r tests
dockerfiles: $(addprefix Dockerfile-,$(PYVERSIONS)) ## Generate dockerfiles for each python version
@unlink Dockerfile >/dev/null
ln -s Dockerfile-$(PYMAIN) Dockerfile
Dockerfile-%: Dockerfile.template ## Generate a specific dockerfile (e.g. Dockerfile-2.7)
sed "s/{{PYVERSION}}/$*/" Dockerfile.template > Dockerfile-$*
quick_build: $(addprefix build-, $(PYMAIN))
build: $(addprefix build-, $(PYVERSIONS)) ## Build all images / python versions
build-%: version Dockerfile-% ## Build a specific version (e.g. build-2.7)
docker build -t '$(IMAGEWTAG)-python$*' -f Dockerfile-$* .;
dev-%: ## Launch a specific development environment using docker (e.g. dev-2.7)
@docker start $(NAME)-dev$* || (\
$(MAKE) build-$*; \
docker run -d -w /usr/src/app/ -p $(DEVPORT):5000 -v $$PWD:/usr/src/app --entrypoint=/bin/bash -ti --name $(NAME)-dev$* '$(IMAGEWTAG)-python$*'; \
)\
docker exec -ti $(NAME)-dev$* bash
dev: dev-$(PYMAIN) ## Launch a development environment using docker, using the default python version
quick_test: test-$(PYMAIN)
test-%: build-% ## Run setup.py from in an isolated container, built from the base image. (e.g. test-2.7)
# This speeds tests up because the image has most (if not all) of the dependencies already.
docker rm $(NAME)-test-$* || true
docker create -ti --name $(NAME)-test-$* --entrypoint="" -w /usr/src/app/ $(IMAGEWTAG)-python$* python setup.py test
docker cp . $(NAME)-test-$*:/usr/src/app
docker start -a $(NAME)-test-$*
test: $(addprefix test-,$(PYVERSIONS)) ## Run the tests with the main python version
run-%: build-%
docker run --rm -p $(DEVPORT):5000 -ti '$(IMAGEWTAG)-python$(PYMAIN)' --default-plugins
run: run-$(PYMAIN)
# Pypy - Upload a package
dist/$(TARNAME): version
python setup.py sdist;
sdist: dist/$(TARNAME) ## Generate the distribution file (wheel)
pip_test-%: sdist ## Test the distribution file using pip install and a specific python version (e.g. pip_test-2.7)
docker run --rm -v $$PWD/dist:/dist/ python:$* pip install /dist/$(TARNAME);
pip_test: $(addprefix pip_test-,$(PYVERSIONS)) ## Test pip installation with the main python version
pip_upload: pip_test ## Upload package to pip
python setup.py sdist upload ;
# Pushing to docker
push-latest: $(addprefix push-latest-,$(PYVERSIONS)) ## Push the "latest" tag to dockerhub
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGEWTAG)'
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGENAME)'
docker push '$(IMAGENAME):latest'
push-latest-%: build-% ## Push the latest image for a specific python version
docker tag $(IMAGENAME):$(VERSION)-python$* $(IMAGENAME):python$*
docker push $(IMAGENAME):$(VERSION)-python$*
docker push $(IMAGENAME):python$*
push-%: build-% ## Push the image of the current version (tagged). e.g. push-2.7
docker push $(IMAGENAME):$(VERSION)-python$*
push:: $(addprefix push-,$(PYVERSIONS)) ## Push an image with the current version for every python version
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGEWTAG)'
docker push $(IMAGENAME):$(VERSION)
clean:: ## Clean older docker images and containers related to this project and dev environments
@docker stop $(addprefix $(NAME)-dev,$(PYVERSIONS)) 2>/dev/null || true
@docker rm $(addprefix $(NAME)-dev,$(PYVERSIONS)) 2>/dev/null || true
@docker ps -a | grep $(IMAGENAME) | awk '{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1;}}' | xargs docker rm -v 2>/dev/null|| true
@docker images | grep $(IMAGENAME) | awk '{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1":"$$2;}}' | xargs docker rmi 2>/dev/null|| true
.PHONY:: yapf dockerfiles Dockerfile-% quick_build build build-% dev-% quick-dev test quick_test push-latest push-latest-% push-% push version .FORCE

View File

@@ -2,10 +2,6 @@ from python:{{PYVERSION}}
MAINTAINER J. Fernando Sánchez <jf.sanchez@upm.es> MAINTAINER J. Fernando Sánchez <jf.sanchez@upm.es>
RUN apt-get update && apt-get install -y \
libblas-dev liblapack-dev liblapacke-dev gfortran \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /cache/ /senpy-plugins /data/ RUN mkdir /cache/ /senpy-plugins /data/
VOLUME /data/ VOLUME /data/
@@ -18,9 +14,9 @@ ONBUILD WORKDIR /senpy-plugins/
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY test-requirements.txt requirements.txt extra-requirements.txt /usr/src/app/ COPY test-requirements.txt requirements.txt /usr/src/app/
RUN pip install --no-cache-dir -r test-requirements.txt -r requirements.txt -r extra-requirements.txt RUN pip install --use-wheel -r test-requirements.txt -r requirements.txt
COPY . /usr/src/app/ COPY . /usr/src/app/
RUN pip install --no-cache-dir --no-index --no-deps --editable . RUN pip install --no-index --no-deps --editable .
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"] ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]

View File

@@ -1,6 +1,5 @@
include requirements.txt include requirements.txt
include test-requirements.txt include test-requirements.txt
include extra-requirements.txt
include README.rst include README.rst
include senpy/VERSION include senpy/VERSION
graft senpy/plugins graft senpy/plugins

140
Makefile
View File

@@ -1,17 +1,147 @@
NAME=senpy NAME=senpy
VERSION=$(shell git describe --tags --dirty 2>/dev/null)
GITHUB_REPO=git@github.com:gsi-upm/senpy.git GITHUB_REPO=git@github.com:gsi-upm/senpy.git
IMAGENAME=gsiupm/senpy IMAGENAME=gsiupm/senpy
IMAGEWTAG=$(IMAGENAME):$(VERSION)
# The first version is the main one (used for quick builds)
# See .makefiles/python.mk for more info
PYVERSIONS=3.5 2.7 PYVERSIONS=3.5 2.7
PYMAIN=$(firstword $(PYVERSIONS))
DEVPORT=5000 DEVPORT=5000
TARNAME=$(NAME)-$(VERSION).tar.gz
action="test-${PYMAIN}" action="test-${PYMAIN}"
GITHUB_REPO=git@github.com:gsi-upm/senpy.git GITHUB_REPO=git@github.com:gsi-upm/senpy.git
include .makefiles/base.mk KUBE_CA_PEM_FILE=""
include .makefiles/k8s.mk KUBE_URL=""
include .makefiles/python.mk KUBE_TOKEN=""
KUBE_NAMESPACE=$(NAME)
KUBECTL=docker run --rm -v $(KUBE_CA_PEM_FILE):/tmp/ca.pem -v $$PWD:/tmp/cwd/ -i lachlanevenson/k8s-kubectl --server="$(KUBE_URL)" --token="$(KUBE_TOKEN)" --certificate-authority="/tmp/ca.pem" -n $(KUBE_NAMESPACE)
CI_REGISTRY=docker.io
CI_REGISTRY_USER=gitlab
CI_BUILD_TOKEN=""
CI_COMMIT_REF_NAME=master
all: build run
.FORCE:
version: .FORCE
@echo $(VERSION) > $(NAME)/VERSION
@echo $(VERSION)
yapf:
yapf -i -r $(NAME)
yapf -i -r tests
init:
pip install --user pre-commit
pre-commit install
dockerfiles: $(addprefix Dockerfile-,$(PYVERSIONS))
@unlink Dockerfile >/dev/null
ln -s Dockerfile-$(PYMAIN) Dockerfile
Dockerfile-%: Dockerfile.template
sed "s/{{PYVERSION}}/$*/" Dockerfile.template > Dockerfile-$*
quick_build: $(addprefix build-, $(PYMAIN))
build: $(addprefix build-, $(PYVERSIONS))
build-%: version Dockerfile-%
docker build -t '$(IMAGEWTAG)-python$*' --cache-from $(IMAGENAME):python$* -f Dockerfile-$* .;
quick_test: $(addprefix test-,$(PYMAIN))
dev-%:
@docker start $(NAME)-dev$* || (\
$(MAKE) build-$*; \
docker run -d -w /usr/src/app/ -p $(DEVPORT):5000 -v $$PWD:/usr/src/app --entrypoint=/bin/bash -ti --name $(NAME)-dev$* '$(IMAGEWTAG)-python$*'; \
)\
docker exec -ti $(NAME)-dev$* bash
dev: dev-$(PYMAIN)
test-all: $(addprefix test-,$(PYVERSIONS))
test-%:
docker run --rm --entrypoint /usr/local/bin/python -w /usr/src/app $(IMAGENAME):python$* setup.py test
test: test-$(PYMAIN)
dist/$(TARNAME): version
python setup.py sdist;
sdist: dist/$(TARNAME)
pip_test-%: sdist
docker run --rm -v $$PWD/dist:/dist/ python:$* pip install /dist/$(TARNAME);
pip_test: $(addprefix pip_test-,$(PYVERSIONS))
pip_upload: pip_test
python setup.py sdist upload ;
clean:
@docker ps -a | grep $(IMAGENAME) | awk '{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1;}}' | xargs docker rm -v 2>/dev/null|| true
@docker images | grep $(IMAGENAME) | awk '{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1":"$$2;}}' | xargs docker rmi 2>/dev/null|| true
@docker stop $(addprefix $(NAME)-dev,$(PYVERSIONS)) 2>/dev/null || true
@docker rm $(addprefix $(NAME)-dev,$(PYVERSIONS)) 2>/dev/null || true
git_commit:
git commit -a
git_tag:
git tag ${VERSION}
git_push:
git push --tags origin master
run-%: build-%
docker run --rm -p $(DEVPORT):5000 -ti '$(IMAGEWTAG)-python$(PYMAIN)' --default-plugins
run: run-$(PYMAIN)
push-latest: $(addprefix push-latest-,$(PYVERSIONS))
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGEWTAG)'
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGENAME)'
docker push '$(IMAGENAME):latest'
docker push '$(IMAGEWTAG)'
push-latest-%: build-%
docker tag $(IMAGENAME):$(VERSION)-python$* $(IMAGENAME):python$*
docker push $(IMAGENAME):$(VERSION)-python$*
docker push $(IMAGENAME):python$*
push-%: build-%
docker push $(IMAGENAME):$(VERSION)-python$*
push: $(addprefix push-,$(PYVERSIONS))
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGEWTAG)'
docker push $(IMAGENAME):$(VERSION)
push-github:
$(eval KEY_FILE := $(shell mktemp))
@echo "$$GITHUB_DEPLOY_KEY" > $(KEY_FILE)
@git remote rm github-deploy || true
git remote add github-deploy $(GITHUB_REPO)
@GIT_SSH_COMMAND="ssh -i $(KEY_FILE)" git fetch github-deploy $(CI_COMMIT_REF_NAME) || true
@GIT_SSH_COMMAND="ssh -i $(KEY_FILE)" git push github-deploy $(CI_COMMIT_REF_NAME)
rm $(KEY_FILE)
ci:
gitlab-runner exec docker --docker-volumes /var/run/docker.sock:/var/run/docker.sock --env CI_PROJECT_NAME=$(NAME) ${action}
deploy:
@$(KUBECTL) delete secret $(CI_REGISTRY) || true
@$(KUBECTL) create secret docker-registry $(CI_REGISTRY) --docker-server=$(CI_REGISTRY) --docker-username=$(CI_REGISTRY_USER) --docker-email=$(CI_REGISTRY_USER) --docker-password=$(CI_BUILD_TOKEN)
@$(KUBECTL) apply -f /tmp/cwd/k8s/
.PHONY: test test-% test-all build-% build test pip_test run yapf push-main push-% dev ci version .FORCE deploy

View File

@@ -1,5 +1,5 @@
.. image:: img/header.png .. image:: img/header.png
:width: 100% :height: 6em
:target: http://demos.gsi.dit.upm.es/senpy :target: http://demos.gsi.dit.upm.es/senpy
.. image:: https://travis-ci.org/gsi-upm/senpy.svg?branch=master .. image:: https://travis-ci.org/gsi-upm/senpy.svg?branch=master

View File

@@ -1,106 +0,0 @@
Client
======
Demo Endpoint
-------------
Import Client and send a request
.. code:: python
from senpy.client import Client
c = Client('http://latest.senpy.cluster.gsi.dit.upm.es/api')
r = c.analyse('I like Pizza', algorithm='sentiment140')
Print response
.. code:: python
for entry in r.entries:
print('{} -> {}'.format(entry['text'], entry['sentiments'][0]['marl:hasPolarity']))
.. parsed-literal::
I like Pizza -> marl:Positive
Obtain a list of available plugins
.. code:: python
for plugin in c.request('/plugins')['plugins']:
print(plugin['name'])
.. parsed-literal::
emoRand
rand
sentiment140
Local Endpoint
--------------
Run a docker container with Senpy image and default plugins
.. code::
docker run -ti --name 'SenpyEndpoint' -d -p 5000:5000 gsiupm/senpy:0.8.6 --host 0.0.0.0 --default-plugins
.. parsed-literal::
a0157cd98057072388bfebeed78a830da7cf0a796f4f1a3fd9188f9f2e5fe562
Import client and send a request to localhost
.. code:: python
c_local = Client('http://127.0.0.1:5000/api')
r = c_local.analyse('Hello world', algorithm='sentiment140')
Print response
.. code:: python
for entry in r.entries:
print('{} -> {}'.format(entry['text'], entry['sentiments'][0]['marl:hasPolarity']))
.. parsed-literal::
Hello world -> marl:Neutral
Obtain a list of available plugins deployed locally
.. code:: python
c_local.plugins().keys()
.. parsed-literal::
rand
sentiment140
emoRand
Stop the docker container
.. code:: python
!docker stop SenpyEndpoint
!docker rm SenpyEndpoint
.. parsed-literal::
SenpyEndpoint
SenpyEndpoint

View File

@@ -1,4 +1,4 @@
{ {
"@type": "plugins", "plugins": [
"plugins": {} ]
} }

View File

@@ -8,7 +8,8 @@
"me:EmotionAnalysis1", "me:EmotionAnalysis1",
"me:NER1", "me:NER1",
{ {
"description": "missing @id and @type" "@type": "analysis",
"@id": "wrong"
} }
], ],
"entries": [ "entries": [

View File

@@ -1,78 +0,0 @@
{
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
"@id": "me:Result1",
"@type": "results",
"analysis": [
"me:SAnalysis1",
"me:SgAnalysis1",
"me:EmotionAnalysis1",
"me:NER1",
{
"@type": "analysis",
"@id": "anonymous"
}
],
"entries": [
{
"@id": "http://micro.blog/status1",
"@type": [
"nif:RFC5147String",
"nif:Context"
],
"nif:isString": "Dear Microsoft, put your Windows Phone on your newest #open technology program. You'll be awesome. #opensource",
"entities": [
{
"@id": "http://micro.blog/status1#char=5,13",
"nif:beginIndex": 5,
"nif:endIndex": 13,
"nif:anchorOf": "Microsoft",
"me:references": "http://dbpedia.org/page/Microsoft",
"prov:wasGeneratedBy": "me:NER1"
},
{
"@id": "http://micro.blog/status1#char=25,37",
"nif:beginIndex": 25,
"nif:endIndex": 37,
"nif:anchorOf": "Windows Phone",
"me:references": "http://dbpedia.org/page/Windows_Phone",
"prov:wasGeneratedBy": "me:NER1"
}
],
"suggestions": [
{
"@id": "http://micro.blog/status1#char=16,77",
"nif:beginIndex": 16,
"nif:endIndex": 77,
"nif:anchorOf": "put your Windows Phone on your newest #open technology program",
"prov:wasGeneratedBy": "me:SgAnalysis1"
}
],
"sentiments": [
{
"@id": "http://micro.blog/status1#char=80,97",
"nif:beginIndex": 80,
"nif:endIndex": 97,
"nif:anchorOf": "You'll be awesome.",
"marl:hasPolarity": "marl:Positive",
"marl:polarityValue": 0.9,
"prov:wasGeneratedBy": "me:SAnalysis1"
}
],
"emotions": [
{
"@id": "http://micro.blog/status1#char=0,109",
"nif:anchorOf": "Dear Microsoft, put your Windows Phone on your newest #open technology program. You'll be awesome. #opensource",
"prov:wasGeneratedBy": "me:EAnalysis1",
"onyx:hasEmotion": [
{
"onyx:hasEmotionCategory": "wna:liking"
},
{
"onyx:hasEmotionCategory": "wna:excitement"
}
]
}
]
}
]
}

View File

@@ -3,7 +3,10 @@
"@id": "me:Result1", "@id": "me:Result1",
"@type": "results", "@type": "results",
"analysis": [ "analysis": [
"me:SgAnalysis1" {
"@id": "me:SgAnalysis1",
"@type": "me:SuggestionAnalysis"
}
], ],
"entries": [ "entries": [
{ {

View File

@@ -1,113 +0,0 @@
Advanced plugin definition
--------------------------
In addition to finding plugins defined in source code files, senpy can also load a special type of definition file (`.senpy` files).
This used to be the only mechanism for loading in earlier versions of senpy.
The definition file contains basic information
Lastly, it is also possible to add new plugins programmatically.
.. contents:: :local:
What is a plugin?
=================
A plugin is a program that, given a text, will add annotations to it.
In practice, a plugin consists of at least two files:
- Definition file: a `.senpy` file that describes the plugin (e.g. what input parameters it accepts, what emotion model it uses).
- Python module: the actual code that will add annotations to each input.
This separation allows us to deploy plugins that use the same code but employ different parameters.
For instance, one could use the same classifier and processing in several plugins, but train with different datasets.
This scenario is particularly useful for evaluation purposes.
The only limitation is that the name of each plugin needs to be unique.
Definition files
================
The definition file complements and overrides the attributes provided by the plugin.
It can be written in YAML or JSON.
The most important attributes are:
* **name**: unique name that senpy will use internally to identify the plugin.
* **module**: indicates the module that contains the plugin code, which will be automatically loaded by senpy.
* **version**
* extra_params: to add parameters to the senpy API when this plugin is requested. Those parameters may be required, and have aliased names. For instance:
.. code:: yaml
extra_params:
hello_param:
aliases: # required
- hello_param
- hello
required: true
default: Hi you
values:
- Hi you
- Hello y'all
- Howdy
A complete example:
.. code:: yaml
name: <Name of the plugin>
module: <Python file>
version: 0.1
And the json equivalent:
.. code:: json
{
"name": "<Name of the plugin>",
"module": "<Python file>",
"version": "0.1"
}
Example plugin with a definition file
=====================================
In this section, we will implement a basic sentiment analysis plugin.
To determine the polarity of each entry, the plugin will compare the length of the string to a threshold.
This threshold will be included in the definition file.
The definition file would look like this:
.. code:: yaml
name: helloworld
module: helloworld
version: 0.0
threshold: 10
description: Hello World
Now, in a file named ``helloworld.py``:
.. code:: python
#!/bin/env python
#helloworld.py
from senpy import AnalysisPlugin
from senpy import Sentiment
class HelloWorld(AnalysisPlugin):
def analyse_entry(entry, params):
'''Basically do nothing with each entry'''
sentiment = Sentiment()
if len(entry.text) < self.threshold:
sentiment['marl:hasPolarity'] = 'marl:Positive'
else:
sentiment['marl:hasPolarity'] = 'marl:Negative'
entry.sentiments.append(sentiment)
yield entry
The complete code of the example plugin is available `here <https://lab.cluster.gsi.dit.upm.es/senpy/plugin-prueba>`__.

View File

@@ -1,8 +1,6 @@
Developing new plugins Developing new plugins
---------------------- ----------------------
This document contains the minimum to get you started with developing new analysis plugin. This document describes how to develop a new analysis plugin. For an example of conversion plugins, see :doc:`conversion`.
For an example of conversion plugins, see :doc:`conversion`.
For a description of definition files, see :doc:`plugins-definition`.
A more step-by-step tutorial with slides is available `here <https://lab.cluster.gsi.dit.upm.es/senpy/senpy-tutorial>`__ A more step-by-step tutorial with slides is available `here <https://lab.cluster.gsi.dit.upm.es/senpy/senpy-tutorial>`__
@@ -11,29 +9,83 @@ A more step-by-step tutorial with slides is available `here <https://lab.cluster
What is a plugin? What is a plugin?
================= =================
A plugin is a python object that can process entries. Given an entry, it will modify it, add annotations to it, or generate new entries. A plugin is a program that, given a text, will add annotations to it.
In practice, a plugin consists of at least two files:
- Definition file: a `.senpy` file that describes the plugin (e.g. what input parameters it accepts, what emotion model it uses).
- Python module: the actual code that will add annotations to each input.
This separation allows us to deploy plugins that use the same code but employ different parameters.
For instance, one could use the same classifier and processing in several plugins, but train with different datasets.
This scenario is particularly useful for evaluation purposes.
The only limitation is that the name of each plugin needs to be unique.
Plugin Definition files
=======================
The definition file contains all the attributes of the plugin, and can be written in YAML or JSON.
When the server is launched, it will recursively search for definition files in the plugin folder (the current folder, by default).
The most important attributes are:
* **name**: unique name that senpy will use internally to identify the plugin.
* **module**: indicates the module that contains the plugin code, which will be automatically loaded by senpy.
* **version**
* extra_params: to add parameters to the senpy API when this plugin is requested. Those parameters may be required, and have aliased names. For instance:
.. code:: yaml
extra_params:
hello_param:
aliases: # required
- hello_param
- hello
required: true
default: Hi you
values:
- Hi you
- Hello y'all
- Howdy
Parameter validation will fail if a required parameter without a default has not been provided, or if the definition includes a set of values and the provided one does not match one of them.
What is an entry? A complete example:
=================
.. code:: yaml
name: <Name of the plugin>
module: <Python file>
version: 0.1
And the json equivalent:
.. code:: json
{
"name": "<Name of the plugin>",
"module": "<Python file>",
"version": "0.1"
}
Plugins Code
============
The basic methods in a plugin are:
* __init__
* activate: used to load memory-hungry resources
* deactivate: used to free up resources
* analyse_entry: called in every user requests. It takes two parameters: ``Entry``, the entry object, and ``params``, the parameters supplied by the user. It should yield one or more ``Entry`` objects.
Plugins are loaded asynchronously, so don't worry if the activate method takes too long. The plugin will be marked as activated once it is finished executing the method.
Entries
=======
Entries are objects that can be annotated. Entries are objects that can be annotated.
In general, they will be a piece of text.
By default, entries are `NIF contexts <http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core/nif-core.html>`_ represented in JSON-LD format. By default, entries are `NIF contexts <http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core/nif-core.html>`_ represented in JSON-LD format.
It is a dictionary/JSON object that looks like this:
.. code:: python
{
"@id": "<unique identifier or blank node name>",
"nif:isString": "input text",
"sentiments": [ {
...
}
],
...
}
Annotations are added to the object like this: Annotations are added to the object like this:
.. code:: python .. code:: python
@@ -48,111 +100,96 @@ The value may be any valid JSON-LD dictionary.
For simplicity, senpy includes a series of models by default in the ``senpy.models`` module. For simplicity, senpy includes a series of models by default in the ``senpy.models`` module.
What are annotations? Example plugin
===================== ==============
They are objects just like entries.
Senpy ships with several default annotations, including: ``Sentiment``, ``Emotion``, ``EmotionSet``...jk bb
In this section, we will implement a basic sentiment analysis plugin.
To determine the polarity of each entry, the plugin will compare the length of the string to a threshold.
This threshold will be included in the definition file.
What's a plugin made of? The definition file would look like this:
========================
When receiving a query, senpy selects what plugin or plugins should process each entry, and in what order. .. code:: yaml
It also makes sure the every entry and the parameters provided by the user meet the plugin requirements.
Hence, two parts are necessary: 1) the code that will process the entry, and 2) some attributes and metadata that will tell senpy how to interact with the plugin. name: helloworld
module: helloworld
version: 0.0
threshold: 10
description: Hello World
In practice, this is what a plugin looks like, tests included: Now, in a file named ``helloworld.py``:
.. literalinclude:: ../senpy/plugins/example/rand_plugin.py
:emphasize-lines: 5-11
:language: python
The lines highlighted contain some information about the plugin.
In particular, the following information is mandatory:
* A unique name for the class. In our example, Rand.
* The subclass/type of plugin. This is typically either `SentimentPlugin` or `EmotionPlugin`. However, new types of plugin can be created for different annotations. The only requirement is that these new types inherit from `senpy.Analysis`
* A description of the plugin. This can be done simply by adding a doc to the class.
* A version, which should get updated.
* An author name.
Plugins Code
============
The basic methods in a plugin are:
* analyse_entry: called in every user requests. It takes two parameters: ``Entry``, the entry object, and ``params``, the parameters supplied by the user. It should yield one or more ``Entry`` objects.
* activate: used to load memory-hungry resources. For instance, to train a classifier.
* deactivate: used to free up resources when the plugin is no longer needed.
Plugins are loaded asynchronously, so don't worry if the activate method takes too long. The plugin will be marked as activated once it is finished executing the method.
How does senpy find modules?
============================
Senpy looks for files of two types:
* Python files of the form `senpy_<NAME>.py` or `<NAME>_plugin.py`. In these files, it will look for: 1) Instances that inherit from `senpy.Plugin`, or subclasses of `senpy.Plugin` that can be initialized without a configuration file. i.e. classes that contain all the required attributes for a plugin.
* Plugin definition files (see :doc:`advanced-plugins`)
Defining additional parameters
==============================
Your plugin may ask for additional parameters from the users of the service by using the attribute ``extra_params`` in your plugin definition.
It takes a dictionary, where the keys are the name of the argument/parameter, and the value has the following fields:
* aliases: the different names which can be used in the request to use the parameter.
* required: if set to true, users need to provide this parameter unless a default is set.
* options: the different acceptable values of the parameter (i.e. an enum). If set, the value provided must match one of the options.
* default: the default value of the parameter, if none is provided in the request.
.. code:: python .. code:: python
"extra_params":{ #!/bin/env python
"language": { #helloworld.py
"aliases": ["language", "lang", "l"],
"required": True, from senpy.plugins import AnalysisPlugin
"options": ["es", "en"], from senpy.models import Sentiment
"default": "es"
}
}
class HelloWorld(AnalysisPlugin):
def analyse_entry(entry, params):
'''Basically do nothing with each entry'''
sentiment = Sentiment()
if len(entry.text) < self.threshold:
sentiment['marl:hasPolarity'] = 'marl:Positive'
else:
sentiment['marl:hasPolarity'] = 'marl:Negative'
entry.sentiments.append(sentiment)
yield entry
The complete code of the example plugin is available `here <https://lab.cluster.gsi.dit.upm.es/senpy/plugin-prueba>`__.
Loading data and files Loading data and files
====================== ======================
Most plugins will need access to files (dictionaries, lexicons, etc.). Most plugins will need access to files (dictionaries, lexicons, etc.).
These files are usually heavy or under a license that does not allow redistribution. It is good practice to specify the paths of these files in the plugin configuration, so the same code can be reused with different resources.
For this reason, senpy has a `data_folder` that is separated from the source files.
The location of this folder is controlled programmatically or by setting the `SENPY_DATA` environment variable.
Plugins have a convenience function `self.open` which will automatically prepend the data folder to relative paths:
.. code:: yaml
name: dictworld
module: dictworld
dictionary_path: <PATH OF THE FILE>
The path can be either absolute, or relative.
From absolute paths
???????????????????
Absolute paths (such as ``/data/dictionary.csv`` are straightfoward:
.. code:: python .. code:: python
import os with open(os.path.join(self.dictionary_path) as f:
...
From relative paths
???????????????????
Since plugins are loading dynamically, relative paths will refer to the current working directory.
Instead, what you usually want is to load files *relative to the plugin source folder*, like so:
class PluginWithResources(AnalysisPlugin): ::
file_in_data = <FILE PATH>
file_in_sources = <FILE PATH>
def activate(self): .
with self.open(self.file_in_data) as f: ..
self._classifier = train_from_file(f) plugin.senpy
file_in_source = os.path.join(self.get_folder(), self.file_in_sources) plugin.py
with self.open(file_in_source) as f: dictionary.csv
pass
For this, we need to first get the path of your source folder first, like so:
It is good practice to specify the paths of these files in the plugin configuration, so the same code can be reused with different resources. .. code:: python
import os
root = os.path.realpath(__file__)
with open(os.path.join(root, self.dictionary_path) as f:
...
Docker image Docker image
@@ -162,16 +199,7 @@ Add the following dockerfile to your project to generate a docker image with you
.. code:: dockerfile .. code:: dockerfile
FROM gsiupm/senpy FROM gsiupm/senpy:0.8.8
Once you make sure your plugin works with a specific version of senpy, modify that file to make sure your build will work even if senpy gets updated.
e.g.:
.. code:: dockerfile
FROM gsiupm/senpy:1.0.1
This will copy your source folder to the image, and install all dependencies. This will copy your source folder to the image, and install all dependencies.
Now, to build an image: Now, to build an image:
@@ -187,7 +215,7 @@ And you can run it with:
docker run -p 5000:5000 gsiupm/exampleplugin docker run -p 5000:5000 gsiupm/exampleplugin
If the plugin uses non-source files (:ref:`loading data and files`), the recommended way is to use `SENPY_DATA` folder. If the plugin non-source files (:ref:`loading data and files`), the recommended way is to use absolute paths.
Data can then be mounted in the container or added to the image. Data can then be mounted in the container or added to the image.
The former is recommended for open source plugins with licensed resources, whereas the latter is the most convenient and can be used for private images. The former is recommended for open source plugins with licensed resources, whereas the latter is the most convenient and can be used for private images.
@@ -201,7 +229,7 @@ Adding data to the image:
.. code:: dockerfile .. code:: dockerfile
FROM gsiupm/senpy:1.0.1 FROM gsiupm/senpy:0.8.8
COPY data / COPY data /
F.A.Q. F.A.Q.
@@ -217,7 +245,7 @@ Why does the analyse function yield instead of return?
?????????????????????????????????????????????????????? ??????????????????????????????????????????????????????
This is so that plugins may add new entries to the response or filter some of them. This is so that plugins may add new entries to the response or filter some of them.
For instance, a chunker may split one entry into several. For instance, a `context detection` plugin may add a new entry for each context in the original entry.
On the other hand, a conversion plugin may leave out those entries that do not contain relevant information. On the other hand, a conversion plugin may leave out those entries that do not contain relevant information.
@@ -247,13 +275,11 @@ Training a classifier can be time time consuming. To avoid running the training
def deactivate(self): def deactivate(self):
self.close() self.close()
You can specify a 'shelf_file' in your .senpy file. By default the ShelfMixin creates a file based on the plugin name and stores it in that plugin's folder.
By default the ShelfMixin creates a file based on the plugin name and stores it in that plugin's folder.
However, you can manually specify a 'shelf_file' in your .senpy file.
Shelves may get corrupted if the plugin exists unexpectedly. Shelves may get corrupted if the plugin exists unexpectedly.
A corrupt shelf prevents the plugin from loading. A corrupt shelf prevents the plugin from loading.
If you do not care about the data in the shelf, you can force your plugin to remove the corrupted file and load anyway, set the 'force_shelf' to True in your plugin and start it again. If you do not care about the pickle, you can force your plugin to remove the corrupted file and load anyway, set the 'force_shelf' to True in your .senpy file.
How can I turn an external service into a plugin? How can I turn an external service into a plugin?
????????????????????????????????????????????????? ?????????????????????????????????????????????????
@@ -287,11 +313,50 @@ This example ilustrate how to implement a plugin that accesses the Sentiment140
prefix=p, prefix=p,
marl__hasPolarity=polarity, marl__hasPolarity=polarity,
marl__polarityValue=polarity_value) marl__polarityValue=polarity_value)
sentiment.prov(self) sentiment.prov__wasGeneratedBy = self.id
entry.sentiments.append(sentiment) entry.sentiments.append(sentiment)
yield entry yield entry
Can my plugin require additional parameters from the user?
??????????????????????????????????????????????????????????
You can add extra parameters in the definition file under the attribute ``extra_params``.
It takes a dictionary, where the keys are the name of the argument/parameter, and the value has the following fields:
* aliases: the different names which can be used in the request to use the parameter.
* required: if set to true, users need to provide this parameter unless a default is set.
* options: the different acceptable values of the parameter (i.e. an enum). If set, the value provided must match one of the options.
* default: the default value of the parameter, if none is provided in the request.
.. code:: python
extra_params
language:
aliases:
- language
- lang
- l
required: true,
options:
- es
- en
default: es
This example shows how to introduce a parameter associated with language.
The extraction of this paremeter is used in the analyse method of the Plugin interface.
.. code:: python
lang = params.get("language")
Where can I set up variables for using them in my plugin?
?????????????????????????????????????????????????????????
You can add these variables in the definition file with the structure of attribute-value pairs.
Every field added to the definition file is available to the plugin instance.
Can I activate a DEBUG mode for my plugin? Can I activate a DEBUG mode for my plugin?
??????????????????????????????????????????? ???????????????????????????????????????????
@@ -306,7 +371,7 @@ Additionally, with the ``--pdb`` option you will be dropped into a pdb post mort
.. code:: bash .. code:: bash
python -m pdb yourplugin.py senpy --pdb
Where can I find more code examples? Where can I find more code examples?
???????????????????????????????????? ????????????????????????????????????

View File

@@ -1,11 +1,8 @@
What is Senpy? What is Senpy?
-------------- --------------
Senpy is a framework for text analysis using Linked Data. There are three main applications of Senpy so far: sentiment and emotion analysis, user profiling and entity recoginition. Annotations and Services are compliant with NIF (NLP Interchange Format). Web services can get really complex: data validation, user interaction, formatting, logging., etc.
The figure below summarizes the typical features in an analysis service.
Senpy aims at providing a framework where analysis modules can be integrated easily as plugins, and providing a core functionality for managing tasks such as data validation, user interaction, formatting, logging, translation to linked data, etc.
The figure below summarizes the typical features in a text analysis service.
Senpy implements all the common blocks, so developers can focus on what really matters: great analysis algorithms that solve real problems. Senpy implements all the common blocks, so developers can focus on what really matters: great analysis algorithms that solve real problems.
.. image:: senpy-framework.png .. image:: senpy-framework.png

View File

@@ -7,29 +7,21 @@ The senpy server is launched via the `senpy` command:
usage: senpy [-h] [--level logging_level] [--debug] [--default-plugins] usage: senpy [-h] [--level logging_level] [--debug] [--default-plugins]
[--host HOST] [--port PORT] [--plugins-folder PLUGINS_FOLDER] [--host HOST] [--port PORT] [--plugins-folder PLUGINS_FOLDER]
[--only-install] [--only-list] [--data-folder DATA_FOLDER] [--only-install]
[--threaded] [--version]
Run a Senpy server Run a Senpy server
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
--level logging_level, -l logging_level --level logging_level, -l logging_level
Logging level Logging level
--debug, -d Run the application in debug mode --debug, -d Run the application in debug mode
--default-plugins Load the default plugins --default-plugins Load the default plugins
--host HOST Use 0.0.0.0 to accept requests from any host. --host HOST Use 0.0.0.0 to accept requests from any host.
--port PORT, -p PORT Port to listen on. --port PORT, -p PORT Port to listen on.
--plugins-folder PLUGINS_FOLDER, -f PLUGINS_FOLDER --plugins-folder PLUGINS_FOLDER, -f PLUGINS_FOLDER
Where to look for plugins. Where to look for plugins.
--only-install, -i Do not run a server, only install plugin dependencies --only-install, -i Do not run a server, only install plugin dependencies
--only-list, --list Do not run a server, only list plugins found
--data-folder DATA_FOLDER, --data DATA_FOLDER
Where to look for data. It be set with the SENPY_DATA
environment variable as well.
--threaded Run a threaded server
--version, -v Output the senpy version and exit
When launched, the server will recursively look for plugins in the specified plugins folder (the current working directory by default). When launched, the server will recursively look for plugins in the specified plugins folder (the current working directory by default).

View File

@@ -1,24 +1,8 @@
Vocabularies and model Vocabularies and model
====================== ======================
The model used in Senpy is based on NIF 2.0 [1], which defines a semantic format and API for improving interoperability among natural language processing services. The model used in Senpy is based on the following vocabularies:
Senpy has been applied to sentiment and emotion analysis services using the following vocabularies: * Marl, a vocabulary designed to annotate and describe subjetive opinions expressed on the web or in information systems.
* Onyx, which is built one the same principles as Marl to annotate and describe emotions, and provides interoperability with Emotion Markup Language.
* Marl [2,6], a vocabulary designed to annotate and describe subjetive opinions expressed on the web or in information systems. * NIF 2.0, which defines a semantic format and APO for improving interoperability among natural language processing services
* Onyx [3,5], which is built one the same principles as Marl to annotate and describe emotions, and provides interoperability with Emotion Markup Language.
An overview of the vocabularies and their use can be found in [4].
[1] Guidelines for developing NIF-based NLP services, Final Community Group Report 22 December 2015 Available at: https://www.w3.org/2015/09/bpmlod-reports/nif-based-nlp-webservices/
[2] Marl Ontology Specification, available at http://www.gsi.dit.upm.es/ontologies/marl/
[3] Onyx Ontology Specification, available at http://www.gsi.dit.upm.es/ontologies/onyx/
[4] Iglesias, C. A., Sánchez-Rada, J. F., Vulcu, G., & Buitelaar, P. (2017). Linked Data Models for Sentiment and Emotion Analysis in Social Networks. In Sentiment Analysis in Social Networks (pp. 49-69).
[5] Sánchez-Rada, J. F., & Iglesias, C. A. (2016). Onyx: A linked data approach to emotion representation. Information Processing & Management, 52(1), 99-114.
[6] Westerski, A., Iglesias Fernandez, C. A., & Tapia Rico, F. (2011). Linked opinions: Describing sentiments on the structured web of data.

View File

@@ -1,23 +0,0 @@
This is a collection of plugins that exemplify certain aspects of plugin development with senpy.
The first series of plugins the `basic` ones.
Their starting point is a classification function defined in `basic.py`.
They all include testing and running them as a script will run all tests.
In ascending order of customization, the plugins are:
* Basic is the simplest plugin of all. It leverages the `SentimentBox` Plugin class to create a plugin out of a classification method, and `MappingMixin` to convert the labels from (`pos`, `neg`) to (`marl:Positive`, `marl:Negative`
* Basic_box is just like the previous one, but replaces the mixin with a custom function.
* Basic_configurable is a version of `basic` with a configurable map of emojis for each sentiment.
* Basic_parameterized like `basic_info`, but users set the map in each query (via `extra_parameters`).
* Basic_analyse\_entry uses the more general `analyse_entry` method and adds the annotations individually.
In rest of the plugins show advanced topics:
* mynoop: shows how to add a definition file with external requirements for a plugin. Doing this with a python-only module would require moving all imports of the requirements to their functions, which is considered bad practice.
* Async: a barebones example of training a plugin and analyzing data in parallel.
All of the plugins in this folder include a set of test cases and they are periodically tested with the latest version of senpy.
Additioanlly, for an example of stand-alone plugin that can be tested and deployed with docker, take a look at: lab.cluster.gsi.dit.upm.es/senpy/plugin-example
bbm

View File

@@ -1,23 +0,0 @@
#!/usr/local/bin/python
# coding: utf-8
emoticons = {
'pos': [':)', ':]', '=)', ':D'],
'neg': [':(', ':[', '=(']
}
emojis = {
'pos': ['😁', '😂', '😃', '😄', '😆', '😅', '😄' '😍'],
'neg': ['😢', '😡', '😠', '😞', '😖', '😔', '😓', '😒']
}
def get_polarity(text, dictionaries=[emoticons, emojis]):
polarity = 'marl:Neutral'
for dictionary in dictionaries:
for label, values in dictionary.items():
for emoticon in values:
if emoticon and emoticon in text:
polarity = label
break
return polarity

View File

@@ -1,47 +0,0 @@
#!/usr/local/bin/python
# coding: utf-8
from senpy import easy_test, models, plugins
import basic
class BasicAnalyseEntry(plugins.SentimentPlugin):
'''Equivalent to Basic, implementing the analyse_entry method'''
author = '@balkian'
version = '0.1'
mappings = {
'pos': 'marl:Positive',
'neg': 'marl:Negative',
'default': 'marl:Neutral'
}
def analyse_entry(self, entry, params):
polarity = basic.get_polarity(entry.text)
polarity = self.mappings.get(polarity, self.mappings['default'])
s = models.Sentiment(marl__hasPolarity=polarity)
s.prov(self)
entry.sentiments.append(s)
yield entry
test_cases = [{
'input': 'Hello :)',
'polarity': 'marl:Positive'
}, {
'input': 'So sad :(',
'polarity': 'marl:Negative'
}, {
'input': 'Yay! Emojis 😁',
'polarity': 'marl:Positive'
}, {
'input': 'But no emoticons 😢',
'polarity': 'marl:Negative'
}]
if __name__ == '__main__':
easy_test()

View File

@@ -1,41 +0,0 @@
#!/usr/local/bin/python
# coding: utf-8
from senpy import easy_test, SentimentBox
import basic
class BasicBox(SentimentBox):
''' A modified version of Basic that also does converts annotations manually'''
author = '@balkian'
version = '0.1'
mappings = {
'pos': 'marl:Positive',
'neg': 'marl:Negative',
'default': 'marl:Neutral'
}
def predict_one(self, input):
output = basic.get_polarity(input)
return self.mappings.get(output, self.mappings['default'])
test_cases = [{
'input': 'Hello :)',
'polarity': 'marl:Positive'
}, {
'input': 'So sad :(',
'polarity': 'marl:Negative'
}, {
'input': 'Yay! Emojis 😁',
'polarity': 'marl:Positive'
}, {
'input': 'But no emoticons 😢',
'polarity': 'marl:Negative'
}]
if __name__ == '__main__':
easy_test()

View File

@@ -1,40 +0,0 @@
#!/usr/local/bin/python
# coding: utf-8
from senpy import easy_test, SentimentBox, MappingMixin
import basic
class Basic(MappingMixin, SentimentBox):
'''Provides sentiment annotation using a lexicon'''
author = '@balkian'
version = '0.1'
mappings = {
'pos': 'marl:Positive',
'neg': 'marl:Negative',
'default': 'marl:Neutral'
}
def predict_one(self, input):
return basic.get_polarity(input)
test_cases = [{
'input': 'Hello :)',
'polarity': 'marl:Positive'
}, {
'input': 'So sad :(',
'polarity': 'marl:Negative'
}, {
'input': 'Yay! Emojis 😁',
'polarity': 'marl:Positive'
}, {
'input': 'But no emoticons 😢',
'polarity': 'marl:Negative'
}]
if __name__ == '__main__':
easy_test()

View File

@@ -1,106 +0,0 @@
#!/usr/local/bin/python
# coding: utf-8
from senpy import easy_test, models, plugins
import basic
class Dictionary(plugins.SentimentPlugin):
'''Sentiment annotation using a configurable lexicon'''
author = '@balkian'
version = '0.2'
dictionaries = [basic.emojis, basic.emoticons]
mappings = {'pos': 'marl:Positive', 'neg': 'marl:Negative'}
def analyse_entry(self, entry, params):
polarity = basic.get_polarity(entry.text, self.dictionaries)
if polarity in self.mappings:
polarity = self.mappings[polarity]
s = models.Sentiment(marl__hasPolarity=polarity)
s.prov(self)
entry.sentiments.append(s)
yield entry
test_cases = [{
'input': 'Hello :)',
'polarity': 'marl:Positive'
}, {
'input': 'So sad :(',
'polarity': 'marl:Negative'
}, {
'input': 'Yay! Emojis 😁',
'polarity': 'marl:Positive'
}, {
'input': 'But no emoticons 😢',
'polarity': 'marl:Negative'
}]
class EmojiOnly(Dictionary):
'''Sentiment annotation with a basic lexicon of emojis'''
description = 'A plugin'
dictionaries = [basic.emojis]
test_cases = [{
'input': 'Hello :)',
'polarity': 'marl:Neutral'
}, {
'input': 'So sad :(',
'polarity': 'marl:Neutral'
}, {
'input': 'Yay! Emojis 😁',
'polarity': 'marl:Positive'
}, {
'input': 'But no emoticons 😢',
'polarity': 'marl:Negative'
}]
class EmoticonsOnly(Dictionary):
'''Sentiment annotation with a basic lexicon of emoticons'''
dictionaries = [basic.emoticons]
test_cases = [{
'input': 'Hello :)',
'polarity': 'marl:Positive'
}, {
'input': 'So sad :(',
'polarity': 'marl:Negative'
}, {
'input': 'Yay! Emojis 😁',
'polarity': 'marl:Neutral'
}, {
'input': 'But no emoticons 😢',
'polarity': 'marl:Neutral'
}]
class Salutes(Dictionary):
'''Sentiment annotation with a custom lexicon, for illustration purposes'''
dictionaries = [{
'marl:Positive': ['Hello', '!'],
'marl:Negative': ['Good bye', ]
}]
test_cases = [{
'input': 'Hello :)',
'polarity': 'marl:Positive'
}, {
'input': 'Good bye :(',
'polarity': 'marl:Negative'
}, {
'input': 'Yay! Emojis 😁',
'polarity': 'marl:Positive'
}, {
'input': 'But no emoticons 😢',
'polarity': 'marl:Neutral'
}]
if __name__ == '__main__':
easy_test()

View File

@@ -1,25 +0,0 @@
from senpy import AnalysisPlugin, easy
class Dummy(AnalysisPlugin):
'''This is a dummy self-contained plugin'''
author = '@balkian'
version = '0.1'
def analyse_entry(self, entry, params):
entry['nif:isString'] = entry['nif:isString'][::-1]
entry.reversed = entry.get('reversed', 0) + 1
yield entry
test_cases = [{
'entry': {
'nif:isString': 'Hello',
},
'expected': {
'nif:isString': 'olleH'
}
}]
if __name__ == '__main__':
easy()

View File

@@ -1,40 +0,0 @@
from senpy import AnalysisPlugin, easy
class DummyRequired(AnalysisPlugin):
'''This is a dummy self-contained plugin'''
author = '@balkian'
version = '0.1'
extra_params = {
'example': {
'description': 'An example parameter',
'required': True,
'options': ['a', 'b']
}
}
def analyse_entry(self, entry, params):
entry['nif:isString'] = entry['nif:isString'][::-1]
entry.reversed = entry.get('reversed', 0) + 1
yield entry
test_cases = [{
'entry': {
'nif:isString': 'Hello',
},
'should_fail': True
}, {
'entry': {
'nif:isString': 'Hello',
},
'params': {
'example': 'a'
},
'expected': {
'nif:isString': 'olleH'
}
}]
if __name__ == '__main__':
easy()

View File

@@ -1,24 +0,0 @@
import noop
from senpy.plugins import SentimentPlugin
class NoOp(SentimentPlugin):
'''This plugin does nothing. Literally nothing.'''
version = 0
def analyse_entry(self, entry, *args, **kwargs):
yield entry
def test(self):
print(dir(noop))
super(NoOp, self).test()
test_cases = [{
'entry': {
'nif:isString': 'hello'
},
'expected': {
'nif:isString': 'hello'
}
}]

View File

@@ -1,3 +0,0 @@
module: mynoop
requirements:
- noop

View File

@@ -1,63 +0,0 @@
#!/usr/local/bin/python
# coding: utf-8
from senpy import easy_test, models, plugins
import basic
class ParameterizedDictionary(plugins.SentimentPlugin):
'''This is a basic self-contained plugin'''
author = '@balkian'
version = '0.2'
extra_params = {
'positive-words': {
'description': 'Comma-separated list of words that are considered positive',
'aliases': ['positive'],
'required': True
},
'negative-words': {
'description': 'Comma-separated list of words that are considered negative',
'aliases': ['negative'],
'required': False
}
}
def analyse_entry(self, entry, params):
positive_words = params['positive-words'].split(',')
negative_words = params['negative-words'].split(',')
dictionary = {
'marl:Positive': positive_words,
'marl:Negative': negative_words,
}
polarity = basic.get_polarity(entry.text, [dictionary])
s = models.Sentiment(marl__hasPolarity=polarity)
s.prov(self)
entry.sentiments.append(s)
yield entry
test_cases = [
{
'input': 'Hello :)',
'polarity': 'marl:Positive',
'parameters': {
'positive': "Hello,:)",
'negative': "sad,:()"
}
},
{
'input': 'Hello :)',
'polarity': 'marl:Negative',
'parameters': {
'positive': "",
'negative': "Hello"
}
}
]
if __name__ == '__main__':
easy_test()

View File

@@ -1,33 +0,0 @@
'''
Create a dummy dataset.
Messages with a happy emoticon are labelled positive
Messages with a sad emoticon are labelled negative
'''
import random
dataset = []
vocabulary = ['hello', 'world', 'senpy', 'cool', 'goodbye', 'random', 'text']
emojimap = {
1: [':)', ],
-1: [':(', ]
}
for tag, values in emojimap.items():
for i in range(1000):
msg = ''
for j in range(3):
msg += random.choice(vocabulary)
msg += " "
msg += random.choice(values)
dataset.append([msg, tag])
text = []
labels = []
for i in dataset:
text.append(i[0])
labels.append(i[1])

View File

@@ -1,30 +0,0 @@
from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.model_selection import train_test_split
from mydata import text, labels
X_train, X_test, y_train, y_test = train_test_split(text, labels, test_size=0.12, random_state=42)
from sklearn.naive_bayes import MultinomialNB
count_vec = CountVectorizer(tokenizer=lambda x: x.split())
clf3 = MultinomialNB()
pipeline = Pipeline([('cv', count_vec),
('clf', clf3)])
pipeline.fit(X_train, y_train)
print('Feature names: {}'.format(count_vec.get_feature_names()))
print('Class count: {}'.format(clf3.class_count_))
if __name__ == '__main__':
print('--Results--')
tests = [
(['The sentiment for senpy should be positive :)', ], 1),
(['The sentiment for anything else should be negative :()', ], -1)
]
for features, expected in tests:
result = pipeline.predict(features)
print('Input: {}\nExpected: {}\nGot: {}'.format(features[0], expected, result))

View File

@@ -1,37 +0,0 @@
from senpy import SentimentBox, MappingMixin, easy_test
from mypipeline import pipeline
class PipelineSentiment(MappingMixin, SentimentBox):
'''
This is a pipeline plugin that wraps a classifier defined in another module
(mypipeline).
'''
author = '@balkian'
version = 0.1
maxPolarityValue = 1
minPolarityValue = -1
mappings = {
1: 'marl:Positive',
-1: 'marl:Negative'
}
def predict_one(self, input):
return pipeline.predict([input, ])[0]
test_cases = [
{
'input': 'The sentiment for senpy should be positive :)',
'polarity': 'marl:Positive'
},
{
'input': 'The sentiment for senpy should be negative :(',
'polarity': 'marl:Negative'
}
]
if __name__ == '__main__':
easy_test()

View File

@@ -1,27 +0,0 @@
from senpy.plugins import AnalysisPlugin
from time import sleep
class Sleep(AnalysisPlugin):
'''Dummy plugin to test async'''
author = "@balkian"
version = "0.2"
timeout = 0.05
extra_params = {
"timeout": {
"@id": "timeout_sleep",
"aliases": ["timeout", "to"],
"required": False,
"default": 0
}
}
def activate(self, *args, **kwargs):
sleep(self.timeout)
def analyse_entry(self, entry, params):
sleep(float(params.get("timeout", self.timeout)))
yield entry
def test(self):
pass

View File

@@ -1 +0,0 @@
gsitk

View File

@@ -13,7 +13,7 @@ spec:
spec: spec:
containers: containers:
- name: senpy-latest - name: senpy-latest
image: $IMAGEWTAG image: gsiupm/senpy:latest
imagePullPolicy: Always imagePullPolicy: Always
args: args:
- "--default-plugins" - "--default-plugins"

View File

@@ -2,13 +2,10 @@ Flask>=0.10.1
requests>=2.4.1 requests>=2.4.1
tornado>=4.4.3 tornado>=4.4.3
PyLD>=0.6.5 PyLD>=0.6.5
nltk six
future future
jsonschema jsonschema
jsonref jsonref
PyYAML PyYAML
rdflib rdflib
rdflib-jsonld rdflib-jsonld
numpy
scipy
scikit-learn

View File

@@ -25,10 +25,4 @@ logger = logging.getLogger(__name__)
logger.info('Using senpy version: {}'.format(__version__)) logger.info('Using senpy version: {}'.format(__version__))
from .utils import easy, easy_load, easy_test # noqa: F401
from .models import * # noqa: F401,F403
from .plugins import * # noqa: F401,F403
from .extensions import * # noqa: F401,F403
__all__ = ['api', 'blueprints', 'cli', 'extensions', 'models', 'plugins'] __all__ = ['api', 'blueprints', 'cli', 'extensions', 'models', 'plugins']

View File

@@ -22,11 +22,9 @@ the server.
from flask import Flask from flask import Flask
from senpy.extensions import Senpy from senpy.extensions import Senpy
from senpy.utils import easy_test
import logging import logging
import os import os
import sys
import argparse import argparse
import senpy import senpy
@@ -40,7 +38,7 @@ def main():
'-l', '-l',
metavar='logging_level', metavar='logging_level',
type=str, type=str,
default="WARN", default="INFO",
help='Logging level') help='Logging level')
parser.add_argument( parser.add_argument(
'--debug', '--debug',
@@ -68,7 +66,7 @@ def main():
'--plugins-folder', '--plugins-folder',
'-f', '-f',
type=str, type=str,
default='.', default='plugins',
help='Where to look for plugins.') help='Where to look for plugins.')
parser.add_argument( parser.add_argument(
'--only-install', '--only-install',
@@ -76,24 +74,6 @@ def main():
action='store_true', action='store_true',
default=False, default=False,
help='Do not run a server, only install plugin dependencies') help='Do not run a server, only install plugin dependencies')
parser.add_argument(
'--only-test',
'-t',
action='store_true',
default=False,
help='Do not run a server, just test all plugins')
parser.add_argument(
'--only-list',
'--list',
action='store_true',
default=False,
help='Do not run a server, only list plugins found')
parser.add_argument(
'--data-folder',
'--data',
type=str,
default=None,
help='Where to look for data. It be set with the SENPY_DATA environment variable as well.')
parser.add_argument( parser.add_argument(
'--threaded', '--threaded',
action='store_false', action='store_false',
@@ -108,30 +88,17 @@ def main():
args = parser.parse_args() args = parser.parse_args()
if args.version: if args.version:
print('Senpy version {}'.format(senpy.__version__)) print('Senpy version {}'.format(senpy.__version__))
print(sys.version)
exit(1) exit(1)
logging.basicConfig()
rl = logging.getLogger() rl = logging.getLogger()
rl.setLevel(getattr(logging, args.level)) rl.setLevel(getattr(logging, args.level))
app = Flask(__name__) app = Flask(__name__)
app.debug = args.debug app.debug = args.debug
sp = Senpy(app, args.plugins_folder, sp = Senpy(app, args.plugins_folder, default_plugins=args.default_plugins)
default_plugins=args.default_plugins,
data_folder=args.data_folder)
if args.only_list:
plugins = sp.plugins()
maxwidth = max(len(x.id) for x in plugins)
for plugin in plugins:
import inspect
fpath = inspect.getfile(plugin.__class__)
print('{: <{width}} @ {}'.format(plugin.id, fpath, width=maxwidth))
return
sp.install_deps()
if args.only_install: if args.only_install:
sp.install_deps()
return return
sp.activate_all() sp.activate_all()
if args.only_test:
easy_test(sp.plugins(), debug=args.debug)
return
print('Senpy version {}'.format(senpy.__version__)) print('Senpy version {}'.format(senpy.__version__))
print('Server running on port %s:%d. Ctrl+C to quit' % (args.host, print('Server running on port %s:%d. Ctrl+C to quit' % (args.host,
args.port)) args.port))

View File

@@ -1,49 +1,38 @@
from future.utils import iteritems from future.utils import iteritems
from .models import Error, Results, Entry, from_string from .models import Error
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
API_PARAMS = { API_PARAMS = {
"algorithm": { "algorithm": {
"aliases": ["algorithms", "a", "algo"], "aliases": ["algorithm", "a", "algo"],
"required": False, "required": False,
"description": ("Algorithms that will be used to process the request."
"It may be a list of comma-separated names."),
},
"expanded-jsonld": {
"@id": "expanded-jsonld",
"aliases": ["expanded"],
"options": "boolean",
"required": True,
"default": False
},
"with_parameters": {
"aliases": ['withparameters',
'with-parameters'],
"options": "boolean",
"default": False,
"required": True
}, },
"outformat": { "outformat": {
"@id": "outformat", "@id": "outformat",
"aliases": ["o"], "aliases": ["outformat", "o"],
"default": "json-ld", "default": "json-ld",
"required": True, "required": True,
"options": ["json-ld", "turtle"], "options": ["json-ld", "turtle"],
}, },
"help": { "expanded-jsonld": {
"@id": "help", "@id": "expanded-jsonld",
"description": "Show additional help to know more about the possible parameters", "aliases": ["expanded", "expanded-jsonld"],
"aliases": ["h"],
"required": True, "required": True,
"options": "boolean", "default": 0
"default": False
}, },
"emotionModel": { "emotionModel": {
"@id": "emotionModel", "@id": "emotionModel",
"aliases": ["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",
@@ -53,43 +42,17 @@ API_PARAMS = {
} }
} }
EVAL_PARAMS = {
"algorithm": {
"aliases": ["plug", "p", "plugins", "algorithms", 'algo', 'a', 'plugin'],
"description": "Plugins to be evaluated",
"required": True,
"help": "See activated plugins in /plugins"
},
"dataset": {
"aliases": ["datasets", "data", "d"],
"description": "Datasets to be evaluated",
"required": True,
"help": "See avalaible datasets in /datasets"
}
}
PLUGINS_PARAMS = {
"plugin_type": {
"@id": "pluginType",
"description": 'What kind of plugins to list',
"aliases": ["pluginType"],
"required": True,
"default": 'analysisPlugin'
}
}
WEB_PARAMS = { WEB_PARAMS = {
"inHeaders": { "inHeaders": {
"aliases": ["headers"], "aliases": ["inHeaders", "headers"],
"required": True, "required": True,
"default": False, "default": "0"
"options": "boolean"
}, },
} }
CLI_PARAMS = { CLI_PARAMS = {
"plugin_folder": { "plugin_folder": {
"aliases": ["folder"], "aliases": ["plugin_folder", "folder"],
"required": True, "required": True,
"default": "." "default": "."
}, },
@@ -98,103 +61,71 @@ CLI_PARAMS = {
NIF_PARAMS = { NIF_PARAMS = {
"input": { "input": {
"@id": "input", "@id": "input",
"aliases": ["i"], "aliases": ["i", "input"],
"required": True, "required": True,
"help": "Input text" "help": "Input text"
}, },
"informat": {
"@id": "informat",
"aliases": ["f", "informat"],
"required": False,
"default": "text",
"options": ["turtle", "text", "json-ld"],
},
"intype": { "intype": {
"@id": "intype", "@id": "intype",
"aliases": ["t"], "aliases": ["intype", "t"],
"required": False, "required": False,
"default": "direct", "default": "direct",
"options": ["direct", "url", "file"], "options": ["direct", "url", "file"],
}, },
"informat": {
"@id": "informat",
"aliases": ["f"],
"required": False,
"default": "text",
"options": ["text", "json-ld"],
},
"language": { "language": {
"@id": "language", "@id": "language",
"aliases": ["l"], "aliases": ["language", "l"],
"required": False, "required": False,
}, },
"prefix": { "prefix": {
"@id": "prefix", "@id": "prefix",
"aliases": ["p"], "aliases": ["prefix", "p"],
"required": True, "required": True,
"default": "", "default": "",
}, },
"urischeme": { "urischeme": {
"@id": "urischeme", "@id": "urischeme",
"aliases": ["u"], "aliases": ["urischeme", "u"],
"required": False, "required": False,
"default": "RFC5147String", "default": "RFC5147String",
"options": "RFC5147String" "options": "RFC5147String"
} },
} }
def parse_params(indict, *specs): def parse_params(indict, spec=NIF_PARAMS):
if not specs: logger.debug("Parsing: {}\n{}".format(indict, spec))
specs = [NIF_PARAMS]
logger.debug("Parsing: {}\n{}".format(indict, specs))
outdict = indict.copy() outdict = indict.copy()
wrong_params = {} wrong_params = {}
for spec in specs: for param, options in iteritems(spec):
for param, options in iteritems(spec): if param[0] != "@": # Exclude json-ld properties
for alias in options.get("aliases", []): for alias in options.get("aliases", []):
# Replace each alias with the correct name of the parameter if alias in indict:
if alias in indict and alias != param:
outdict[param] = indict[alias] outdict[param] = indict[alias]
del outdict[alias]
continue
if param not in outdict: if param not in outdict:
if "default" in options: if options.get("required", False) and "default" not in options:
# We assume the default is correct
outdict[param] = options["default"]
elif options.get("required", False):
wrong_params[param] = spec[param] wrong_params[param] = spec[param]
continue else:
if "options" in options: if "default" in options:
if options["options"] == "boolean": outdict[param] = options["default"]
outdict[param] = outdict[param] in [None, True, 'true', '1'] else:
elif outdict[param] not in options["options"]: if "options" in spec[param] and \
outdict[param] not in spec[param]["options"]:
wrong_params[param] = spec[param] wrong_params[param] = spec[param]
if wrong_params: if wrong_params:
logger.debug("Error parsing: %s", wrong_params) logger.debug("Error parsing: %s", wrong_params)
message = Error( message = Error(
status=400, status=400,
message='Missing or invalid parameters', message="Missing or invalid parameters",
parameters=outdict, parameters=outdict,
errors=wrong_params) errors={param: error
for param, error in iteritems(wrong_params)})
raise message raise message
if 'algorithm' in outdict and not isinstance(outdict['algorithm'], list):
outdict['algorithm'] = outdict['algorithm'].split(',')
return outdict return outdict
def parse_extra_params(request, plugin=None):
params = request.parameters.copy()
if plugin:
extra_params = parse_params(params, plugin.get('extra_params', {}))
params.update(extra_params)
return params
def parse_call(params):
'''Return a results object based on the parameters used in a call/request.
'''
params = parse_params(params, NIF_PARAMS)
if params['informat'] == 'text':
results = Results()
entry = Entry(nif__isString=params['input'])
results.entries.append(entry)
elif params['informat'] == 'json-ld':
results = from_string(params['input'], cls=Results)
else: # pragma: no cover
raise NotImplementedError('Informat {} is not implemented'.format(params['informat']))
results.parameters = params
return results

View File

@@ -19,19 +19,18 @@ Blueprints for Senpy
""" """
from flask import (Blueprint, request, current_app, render_template, url_for, from flask import (Blueprint, request, current_app, render_template, url_for,
jsonify) jsonify)
from .models import Error, Response, Help, Plugins, read_schema, dump_schema, Datasets from .models import Error, Response, Plugins, read_schema
from . import api from .api import WEB_PARAMS, API_PARAMS, parse_params
from .version import __version__ from .version import __version__
from functools import wraps from functools import wraps
import logging import logging
import traceback
import json import json
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
api_blueprint = Blueprint("api", __name__) api_blueprint = Blueprint("api", __name__)
demo_blueprint = Blueprint("demo", __name__, template_folder='templates') demo_blueprint = Blueprint("demo", __name__)
ns_blueprint = Blueprint("ns", __name__) ns_blueprint = Blueprint("ns", __name__)
@@ -67,94 +66,68 @@ def index():
@api_blueprint.route('/schemas/<schema>') @api_blueprint.route('/schemas/<schema>')
def schema(schema="definitions"): def schema(schema="definitions"):
try: try:
return dump_schema(read_schema(schema)) return jsonify(read_schema(schema))
except Exception as ex: # Should be FileNotFoundError, but it's missing from py2 except Exception: # Should be FileNotFoundError, but it's missing from py2
return Error(message="Schema not found: {}".format(ex), status=404).flask() return Error(message="Schema not found", status=404).flask()
def basic_api(f): def basic_api(f):
default_params = {
'inHeaders': False,
'expanded-jsonld': False,
'outformat': 'json-ld',
'with_parameters': True,
}
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
raw_params = get_params(request) raw_params = get_params(request)
logger.info('Getting request: {}'.format(raw_params))
headers = {'X-ORIGINAL-PARAMS': json.dumps(raw_params)} headers = {'X-ORIGINAL-PARAMS': json.dumps(raw_params)}
params = default_params # Get defaults
web_params = parse_params({}, spec=WEB_PARAMS)
api_params = parse_params({}, spec=API_PARAMS)
outformat = 'json-ld'
try: try:
params = api.parse_params(raw_params, api.WEB_PARAMS, api.API_PARAMS) print('Getting request:')
if hasattr(request, 'parameters'): print(request)
request.parameters.update(params) web_params = parse_params(raw_params, spec=WEB_PARAMS)
api_params = parse_params(raw_params, spec=API_PARAMS)
if hasattr(request, 'params'):
request.params.update(api_params)
else: else:
request.parameters = params request.params = api_params
response = f(*args, **kwargs) response = f(*args, **kwargs)
except (Exception) as ex: except Error as ex:
response = ex
logger.error(ex)
if current_app.debug: if current_app.debug:
raise raise
if not isinstance(ex, Error):
msg = "{}:\n\t{}".format(ex,
traceback.format_exc())
ex = Error(message=msg, status=500)
logger.exception('Error returning analysis result')
response = ex
response.parameters = raw_params
logger.error(ex)
if 'parameters' in response and not params['with_parameters']: in_headers = web_params['inHeaders'] != "0"
del response.parameters expanded = api_params['expanded-jsonld']
outformat = api_params['outformat']
logger.info('Response: {}'.format(response))
return response.flask( return response.flask(
in_headers=params['inHeaders'], in_headers=in_headers,
headers=headers, headers=headers,
prefix=url_for('.api_root', _external=True), prefix=url_for('.api', _external=True),
context_uri=url_for('api.context', context_uri=url_for('api.context',
entity=type(response).__name__, entity=type(response).__name__,
_external=True), _external=True),
outformat=params['outformat'], outformat=outformat,
expanded=params['expanded-jsonld']) expanded=expanded)
return decorated_function return decorated_function
@api_blueprint.route('/', methods=['POST', 'GET']) @api_blueprint.route('/', methods=['POST', 'GET'])
@basic_api @basic_api
def api_root(): def api():
if request.parameters['help']: response = current_app.senpy.analyse(**request.params)
dic = dict(api.API_PARAMS, **api.NIF_PARAMS) return response
response = Help(valid_parameters=dic)
return response
req = api.parse_call(request.parameters)
return current_app.senpy.analyse(req)
@api_blueprint.route('/evaluate/', methods=['POST', 'GET'])
@basic_api
def evaluate():
if request.parameters['help']:
dic = dict(api.EVAL_PARAMS)
response = Help(parameters=dic)
return response
else:
params = api.parse_params(request.parameters, api.EVAL_PARAMS)
response = current_app.senpy.evaluate(params)
return response
@api_blueprint.route('/plugins/', methods=['POST', 'GET']) @api_blueprint.route('/plugins/', methods=['POST', 'GET'])
@basic_api @basic_api
def plugins(): def plugins():
sp = current_app.senpy sp = current_app.senpy
params = api.parse_params(request.parameters, api.PLUGINS_PARAMS) ptype = request.params.get('plugin_type')
ptype = params.get('plugin_type') plugins = sp.filter_plugins(plugin_type=ptype)
plugins = list(sp.plugins(plugin_type=ptype)) dic = Plugins(plugins=list(plugins.values()))
dic = Plugins(plugins=plugins)
return dic return dic
@@ -162,13 +135,12 @@ def plugins():
@basic_api @basic_api
def plugin(plugin=None): def plugin(plugin=None):
sp = current_app.senpy sp = current_app.senpy
return sp.get_plugin(plugin) if plugin == 'default' and sp.default_plugin:
return sp.default_plugin
plugins = sp.filter_plugins(
@api_blueprint.route('/datasets/', methods=['POST', 'GET']) id='plugins/{}'.format(plugin)) or sp.filter_plugins(name=plugin)
@basic_api if plugins:
def datasets(): response = list(plugins.values())[0]
sp = current_app.senpy else:
datasets = sp.datasets return Error(message="Plugin not found", status=404)
dic = Datasets(datasets=list(datasets.values())) return response
return dic

View File

@@ -1,7 +1,7 @@
import sys import sys
from .models import Error from .models import Error
from .api import parse_params, CLI_PARAMS
from .extensions import Senpy from .extensions import Senpy
from . import api
def argv_to_dict(argv): def argv_to_dict(argv):
@@ -13,31 +13,27 @@ def argv_to_dict(argv):
if argv[i][0] == '-': if argv[i][0] == '-':
key = argv[i].strip('-') key = argv[i].strip('-')
value = argv[i + 1] if len(argv) > i + 1 else None value = argv[i + 1] if len(argv) > i + 1 else None
if not value or value[0] == '-': if value and value[0] == '-':
cli_dict[key] = True cli_dict[key] = ""
else: else:
cli_dict[key] = value cli_dict[key] = value
return cli_dict return cli_dict
def parse_cli(argv):
cli_dict = argv_to_dict(argv)
cli_params = parse_params(cli_dict, spec=CLI_PARAMS)
return cli_params, cli_dict
def main_function(argv): def main_function(argv):
'''This is the method for unit testing '''This is the method for unit testing
''' '''
params = api.parse_params(argv_to_dict(argv), cli_params, cli_dict = parse_cli(argv)
api.CLI_PARAMS, plugin_folder = cli_params['plugin_folder']
api.API_PARAMS, sp = Senpy(default_plugins=False, plugin_folder=plugin_folder)
api.NIF_PARAMS) sp.activate_all(sync=True)
plugin_folder = params['plugin_folder'] res = sp.analyse(**cli_dict)
default_plugins = params.get('default-plugins', False)
sp = Senpy(default_plugins=default_plugins, plugin_folder=plugin_folder)
request = api.parse_call(params)
algos = request.parameters.get('algorithm', None)
if algos:
for algo in algos:
sp.activate_plugin(algo)
else:
sp.activate_all()
res = sp.analyse(request)
return res return res
@@ -46,9 +42,9 @@ def main():
''' '''
try: try:
res = main_function(sys.argv[1:]) res = main_function(sys.argv[1:])
print(res.serialize()) print(res.to_JSON())
except Error as err: except Error as err:
print(err.serialize()) print(err.to_JSON())
sys.exit(2) sys.exit(2)

View File

@@ -1,6 +1,7 @@
import requests import requests
import logging import logging
from . import models from . import models
from .plugins import default_plugin_type
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -12,17 +13,10 @@ class Client(object):
def analyse(self, input, method='GET', **kwargs): def analyse(self, input, method='GET', **kwargs):
return self.request('/', method=method, input=input, **kwargs) return self.request('/', method=method, input=input, **kwargs)
def evaluate(self, input, method='GET', **kwargs): def plugins(self, ptype=default_plugin_type):
return self.request('/evaluate', method = method, input=input, **kwargs) resp = self.request(path='/plugins', plugin_type=ptype).plugins
def plugins(self, *args, **kwargs):
resp = self.request(path='/plugins').plugins
return {p.name: p for p in resp} return {p.name: p for p in resp}
def datasets(self):
resp = self.request(path='/datasets').datasets
return {d.name: d for d in resp}
def request(self, path=None, method='GET', **params): def request(self, path=None, method='GET', **params):
url = '{}{}'.format(self.endpoint, path) url = '{}{}'.format(self.endpoint, path)
response = requests.request(method=method, url=url, params=params) response = requests.request(method=method, url=url, params=params)

View File

@@ -5,59 +5,56 @@ 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, api from . import plugins
from .plugins import Plugin, evaluate from .plugins import SenpyPlugin
from .models import Error, AggregatedEvaluation from .models import Error, Entry, Results, from_string
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 threading import Thread from threading import Thread
from functools import partial
import os import os
import copy import copy
import errno import fnmatch
import inspect
import sys
import importlib
import logging import logging
import traceback
import yaml
import subprocess
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
try:
from gsitk.datasets.datasets import DatasetManager def log_subprocess_output(process):
GSITK_AVAILABLE = True for line in iter(process.stdout.readline, b''):
except ImportError: logger.info('%r', line)
logger.warn('GSITK is not installed. Some functions will be unavailable.') for line in iter(process.stderr.readline, b''):
GSITK_AVAILABLE = False logger.error('%r', line)
class Senpy(object): class Senpy(object):
""" Default Senpy extension for Flask """ """ Default Senpy extension for Flask """
def __init__(self, def __init__(self,
app=None, app=None,
plugin_folder=".", plugin_folder=".",
data_folder=None,
default_plugins=False): default_plugins=False):
self.app = app
default_data = os.path.join(os.getcwd(), 'senpy_data') self._search_folders = set()
self.data_folder = data_folder or os.environ.get('SENPY_DATA', default_data) self._plugin_list = []
try: self._outdated = True
os.makedirs(self.data_folder)
except OSError as e:
if e.errno == errno.EEXIST:
logger.debug('Data folder exists: {}'.format(self.data_folder))
else: # pragma: no cover
raise
self._default = None self._default = None
self._plugins = {}
if plugin_folder:
self.add_folder(plugin_folder)
self.add_folder(plugin_folder)
if default_plugins: if default_plugins:
self.add_folder('plugins', from_root=True) self.add_folder('plugins', from_root=True)
else: else:
# Add only conversion plugins # Add only conversion plugins
self.add_folder(os.path.join('plugins', 'conversion'), self.add_folder(os.path.join('plugins', 'conversion'),
from_root=True) from_root=True)
self.app = app
if app is not None: if app is not None:
self.init_app(app) self.init_app(app)
@@ -72,182 +69,131 @@ class Senpy(object):
# otherwise fall back to the request context # otherwise fall back to the request context
if hasattr(app, 'teardown_appcontext'): if hasattr(app, 'teardown_appcontext'):
app.teardown_appcontext(self.teardown) app.teardown_appcontext(self.teardown)
else: # pragma: no cover else:
app.teardown_request(self.teardown) app.teardown_request(self.teardown)
app.register_blueprint(api_blueprint, url_prefix="/api") app.register_blueprint(api_blueprint, url_prefix="/api")
app.register_blueprint(ns_blueprint, url_prefix="/ns") app.register_blueprint(ns_blueprint, url_prefix="/ns")
app.register_blueprint(demo_blueprint, url_prefix="/") app.register_blueprint(demo_blueprint, url_prefix="/")
def add_plugin(self, plugin):
self._plugins[plugin.name.lower()] = plugin
def delete_plugin(self, plugin):
del self._plugins[plugin.name.lower()]
def plugins(self, **kwargs):
""" Return the plugins registered for a given application. Filtered by criteria """
return list(plugins.pfilter(self._plugins, **kwargs))
def get_plugin(self, name, default=None):
if name == 'default':
return self.default_plugin
plugin = name.lower()
if plugin in self._plugins:
return self._plugins[plugin]
results = self.plugins(id='plugins/{}'.format(name))
if not results:
return Error(message="Plugin not found", status=404)
return results[0]
@property
def analysis_plugins(self):
""" Return only the analysis plugins """
return self.plugins(plugin_type='analysisPlugin')
def add_folder(self, folder, from_root=False): def add_folder(self, folder, from_root=False):
""" Find plugins in this folder and add them to this instance """
if from_root: if from_root:
folder = os.path.join(os.path.dirname(__file__), folder) folder = os.path.join(os.path.dirname(__file__), folder)
logger.debug("Adding folder: %s", folder) logger.debug("Adding folder: %s", folder)
if os.path.isdir(folder): if os.path.isdir(folder):
new_plugins = plugins.from_folder([folder], self._search_folders.add(folder)
data_folder=self.data_folder) self._outdated = True
for plugin in new_plugins:
self.add_plugin(plugin)
else: else:
raise AttributeError("Not a folder or does not exist: %s", folder) logger.debug("Not a folder: %s", folder)
def _get_plugins(self, request): def _find_plugins(self, params):
if not self.analysis_plugins: if not self.analysis_plugins:
raise Error( raise Error(
status=404, status=404,
message=("No plugins found." message=("No plugins found."
" Please install one.")) " Please install one."))
algos = request.parameters.get('algorithm', None) api_params = parse_params(params, spec=API_PARAMS)
if not algos: algos = None
if self.default_plugin: if "algorithm" in api_params and api_params["algorithm"]:
algos = [self.default_plugin.name, ] algos = api_params["algorithm"].split(',')
else: elif self.default_plugin:
raise Error( algos = [self.default_plugin.name, ]
status=404, else:
message="No default plugin found, and None provided") raise Error(
status=404,
message="No default plugin found, and None provided")
plugins = list() plugins = list()
for algo in algos: for algo in algos:
algo = algo.lower() if algo not in self.plugins:
if algo not in self._plugins: logger.debug(("The algorithm '{}' is not valid\n"
msg = ("The algorithm '{}' is not valid\n" "Valid algorithms: {}").format(algo,
"Valid algorithms: {}").format(algo, self.plugins.keys()))
self._plugins.keys())
logger.debug(msg)
raise Error( raise Error(
status=404, status=404,
message=msg) message="The algorithm '{}' is not valid".format(algo))
plugins.append(self._plugins[algo])
if not self.plugins[algo].is_activated:
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 return plugins
def _process_entries(self, entries, req, plugins): def _get_params(self, params, plugin=None):
""" nif_params = parse_params(params, spec=NIF_PARAMS)
Recursively process the entries with the first plugin in the list, and pass the results if plugin:
to the rest of the plugins. extra_params = plugin.get('extra_params', {})
""" specific_params = parse_params(params, spec=extra_params)
nif_params.update(specific_params)
return nif_params
def _get_entries(self, params):
if params['informat'] == 'text':
results = Results()
entry = Entry(text=params['input'])
results.entries.append(entry)
elif params['informat'] == 'json-ld':
results = from_string(params['input'], cls=Results)
else:
raise NotImplemented('Informat {} is not implemented'.format(params['informat']))
return results
def _process_entries(self, entries, plugins, nif_params):
if not plugins: if not plugins:
for i in entries: for i in entries:
yield i yield i
return return
plugin = plugins[0] plugin = plugins[0]
self._activate(plugin) # Make sure the plugin is activated specific_params = self._get_params(nif_params, plugin)
specific_params = api.parse_extra_params(req, plugin)
req.analysis.append({'plugin': plugin,
'parameters': specific_params})
results = plugin.analyse_entries(entries, specific_params) results = plugin.analyse_entries(entries, specific_params)
for i in self._process_entries(results, req, plugins[1:]): for i in self._process_entries(results, plugins[1:], nif_params):
yield i yield i
def install_deps(self): def _process_response(self, resp, plugins, nif_params):
for plugin in self.plugins(is_activated=True): entries = resp.entries
plugins.install_deps(plugin) 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, request): def analyse(self, **api_params):
""" """
Main method that analyses a request, either from CLI or HTTP. Main method that analyses a request, either from CLI or HTTP.
It takes a processed request, provided by the user, as returned It uses a dictionary of parameters, provided by the user.
by api.parse_call().
""" """
logger.debug("analysing request: {}".format(request)) logger.debug("analysing with params: {}".format(api_params))
entries = request.entries plugins = self._find_plugins(api_params)
request.entries = [] nif_params = self._get_params(api_params)
plugins = self._get_plugins(request) resp = self._get_entries(nif_params)
results = request if 'with_parameters' in api_params:
for i in self._process_entries(entries, results, plugins): resp.parameters = nif_params
results.entries.append(i) try:
self.convert_emotions(results) resp = self._process_response(resp, plugins, nif_params)
logger.debug("Returning analysis result: {}".format(results)) self.convert_emotions(resp, plugins, nif_params)
results.analysis = [i['plugin'].id for i in results.analysis] logger.debug("Returning analysis result: {}".format(resp))
return results except (Error, Exception) as ex:
if not isinstance(ex, Error):
def _get_datasets(self, request): ex = Error(message=str(ex), status=500)
if not self.datasets: logger.exception('Error returning analysis result')
raise Error( raise ex
status=404, return resp
message=("No datasets found."
" Please verify DatasetManager"))
datasets_name = request.parameters.get('dataset', None).split(',')
for dataset in datasets_name:
if dataset not in self.datasets:
logger.debug(("The dataset '{}' is not valid\n"
"Valid datasets: {}").format(dataset,
self.datasets.keys()))
raise Error(
status=404,
message="The dataset '{}' is not valid".format(dataset))
dm = DatasetManager()
datasets = dm.prepare_datasets(datasets_name)
return datasets
@property
def datasets(self):
if not GSITK_AVAILABLE:
raise Exception('GSITK is not available. Install it to use this function.')
self._dataset_list = {}
dm = DatasetManager()
for item in dm.get_datasets():
for key in item:
if key in self._dataset_list:
continue
properties = item[key]
properties['@id'] = key
self._dataset_list[key] = properties
return self._dataset_list
def evaluate(self, params):
if not GSITK_AVAILABLE:
raise Exception('GSITK is not available. Install it to use this function.')
logger.debug("evaluating request: {}".format(params))
results = AggregatedEvaluation()
results.parameters = params
datasets = self._get_datasets(results)
plugins = self._get_plugins(results)
for eval in evaluate(plugins, datasets):
results.evaluations.append(eval)
if 'with_parameters' not in results.parameters:
del results.parameters
logger.debug("Returning evaluation result: {}".format(results))
return results
def _conversion_candidates(self, fromModel, toModel): def _conversion_candidates(self, fromModel, toModel):
candidates = self.plugins(plugin_type='emotionConversionPlugin') candidates = self.filter_plugins(plugin_type='emotionConversionPlugin')
for candidate in candidates: for name, candidate in candidates.items():
for pair in candidate.onyx__doesConversion: for pair in candidate.onyx__doesConversion:
logging.debug(pair) logging.debug(pair)
if pair['onyx:conversionFrom'] == fromModel \ if pair['onyx:conversionFrom'] == fromModel \
and pair['onyx:conversionTo'] == toModel: and pair['onyx:conversionTo'] == toModel:
# logging.debug('Found candidate: {}'.format(candidate))
yield candidate yield candidate
def convert_emotions(self, resp): def convert_emotions(self, resp, plugins, params):
""" """
Conversion of all emotions in a response **in place**. Conversion of all emotions in a response **in place**.
In addition to converting from one model to another, it has In addition to converting from one model to another, it has
@@ -255,8 +201,6 @@ 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
""" """
plugins = [i['plugin'] for i in resp.analysis]
params = resp.parameters
toModel = params.get('emotionModel', None) toModel = params.get('emotionModel', None)
if not toModel: if not toModel:
return return
@@ -271,8 +215,7 @@ class Senpy(object):
logger.debug('Analysis plugin {} uses model: {}'.format(plugin.id, fromModel)) logger.debug('Analysis plugin {} uses model: {}'.format(plugin.id, fromModel))
except StopIteration: except StopIteration:
e = Error(('No conversion plugin found for: ' e = Error(('No conversion plugin found for: '
'{} -> {}'.format(fromModel, toModel)), '{} -> {}'.format(fromModel, toModel)))
status=404)
e.original_response = resp e.original_response = resp
e.parameters = params e.parameters = params
raise e raise e
@@ -285,8 +228,7 @@ class Senpy(object):
for j in i.emotions: for j in i.emotions:
plugname = j['prov:wasGeneratedBy'] plugname = j['prov:wasGeneratedBy']
candidate = candidates[plugname] candidate = candidates[plugname]
resp.analysis.append({'plugin': candidate, resp.analysis.append(candidate.id)
'parameters': params})
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':
@@ -295,96 +237,191 @@ class Senpy(object):
i.emotions = newemotions i.emotions = newemotions
newentries.append(i) newentries.append(i)
resp.entries = newentries resp.entries = newentries
resp.analysis = list(set(resp.analysis))
@property @property
def default_plugin(self): def default_plugin(self):
if not self._default or not self._default.is_activated: candidate = self._default
candidates = self.plugins(plugin_type='analysisPlugin', if not candidate:
is_activated=True) candidates = self.filter_plugins(plugin_type='analysisPlugin',
is_activated=True)
if len(candidates) > 0: if len(candidates) > 0:
self._default = candidates[0] candidate = list(candidates.values())[0]
else: logger.debug("Default: {}".format(candidate))
self._default = None return candidate
logger.debug("Default: {}".format(self._default))
return self._default
@default_plugin.setter @default_plugin.setter
def default_plugin(self, value): def default_plugin(self, value):
if isinstance(value, Plugin): if isinstance(value, SenpyPlugin):
if not value.is_activated:
raise AttributeError('The default plugin has to be activated.')
self._default = value self._default = value
else: else:
self._default = self._plugins[value.lower()] self._default = self.plugins[value]
def activate_all(self, sync=True): def activate_all(self, sync=False):
ps = [] ps = []
for plug in self._plugins.keys(): for plug in self.plugins.keys():
ps.append(self.activate_plugin(plug, sync=sync)) ps.append(self.activate_plugin(plug, sync=sync))
return ps return ps
def deactivate_all(self, sync=True): def deactivate_all(self, sync=False):
ps = [] ps = []
for plug in self._plugins.keys(): for plug in self.plugins.keys():
ps.append(self.deactivate_plugin(plug, sync=sync)) ps.append(self.deactivate_plugin(plug, sync=sync))
return ps return ps
def _set_active(self, plugin, active=True, *args, **kwargs): def _set_active_plugin(self, plugin_name, active=True, *args, **kwargs):
''' We're using a variable in the plugin itself to activate/deactivate plugins.\ ''' We're using a variable in the plugin itself to activate/deactive plugins.\
Note that plugins may activate themselves by setting this variable. Note that plugins may activate themselves by setting this variable.
''' '''
plugin.is_activated = active self.plugins[plugin_name].is_activated = active
def _activate(self, plugin): def activate_plugin(self, plugin_name, sync=False):
success = False try:
with plugin._lock: plugin = self.plugins[plugin_name]
if plugin.is_activated: except KeyError:
return
plugin.activate()
msg = "Plugin activated: {}".format(plugin.name)
logger.info(msg)
success = True
self._set_active(plugin, success)
def activate_plugin(self, plugin_name, sync=True):
plugin_name = plugin_name.lower()
if plugin_name not in self._plugins:
raise Error( raise Error(
message="Plugin not found: {}".format(plugin_name), status=404) message="Plugin not found: {}".format(plugin_name), status=404)
plugin = self._plugins[plugin_name]
logger.info("Activating plugin: {}".format(plugin.name)) logger.info("Activating plugin: {}".format(plugin.name))
def act():
success = False
try:
plugin.activate()
msg = "Plugin activated: {}".format(plugin.name)
logger.info(msg)
success = True
self._set_active_plugin(plugin_name, success)
except Exception as ex:
msg = "Error activating plugin {} - {} : \n\t{}".format(
plugin.name, ex, traceback.format_exc())
logger.error(msg)
raise Error(msg)
if sync or 'async' in plugin and not plugin.async: if sync or 'async' in plugin and not plugin.async:
self._activate(plugin) act()
else: else:
th = Thread(target=partial(self._activate, plugin)) th = Thread(target=act)
th.start() th.start()
return th return th
def _deactivate(self, plugin): def deactivate_plugin(self, plugin_name, sync=False):
with plugin._lock: try:
if not plugin.is_activated: plugin = self.plugins[plugin_name]
return except KeyError:
plugin.deactivate()
logger.info("Plugin deactivated: {}".format(plugin.name))
def deactivate_plugin(self, plugin_name, sync=True):
plugin_name = plugin_name.lower()
if plugin_name not in self._plugins:
raise Error( raise Error(
message="Plugin not found: {}".format(plugin_name), status=404) message="Plugin not found: {}".format(plugin_name), status=404)
plugin = self._plugins[plugin_name]
self._set_active(plugin, False) self._set_active_plugin(plugin_name, False)
def deact():
try:
plugin.deactivate()
logger.info("Plugin deactivated: {}".format(plugin.name))
except Exception as ex:
logger.error(
"Error deactivating plugin {}: {}".format(plugin.name, ex))
logger.error("Trace: {}".format(traceback.format_exc()))
if sync or 'async' in plugin and not plugin.async: if sync or 'async' in plugin and not plugin.async:
self._deactivate(plugin) deact()
else: else:
th = Thread(target=partial(self._deactivate, plugin)) th = Thread(target=deact)
th.start() th.start()
return th return th
@classmethod
def validate_info(cls, info):
return all(x in info for x in ('name', 'module', 'description', 'version'))
def install_deps(self):
for i in self.plugins.values():
self._install_deps(i)
@classmethod
def _install_deps(cls, info=None):
requirements = info.get('requirements', [])
if requirements:
pip_args = ['pip']
pip_args.append('install')
pip_args.append('--use-wheel')
for req in requirements:
pip_args.append(req)
logger.info('Installing requirements: ' + str(requirements))
process = subprocess.Popen(pip_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
log_subprocess_output(process)
exitcode = process.wait()
if exitcode != 0:
raise Error("Dependencies not properly installed")
@classmethod
def _load_module(cls, name, root):
sys.path.append(root)
tmp = importlib.import_module(name)
sys.path.remove(root)
return tmp
@classmethod
def _load_plugin_from_info(cls, info, root):
if not cls.validate_info(info):
logger.warn('The module info is not valid.\n\t{}'.format(info))
return None, None
module = info["module"]
name = info["name"]
cls._install_deps(info)
tmp = cls._load_module(module, root)
candidate = None
for _, obj in inspect.getmembers(tmp):
if inspect.isclass(obj) and inspect.getmodule(obj) == tmp:
logger.debug(("Found plugin class:"
" {}@{}").format(obj, inspect.getmodule(obj)))
candidate = obj
break
if not candidate:
logger.debug("No valid plugin for: {}".format(module))
return
module = candidate(info=info)
return name, module
@classmethod
def _load_plugin(cls, root, filename):
fpath = os.path.join(root, filename)
logger.debug("Loading plugin: {}".format(fpath))
with open(fpath, 'r') as f:
info = yaml.load(f)
logger.debug("Info: {}".format(info))
return cls._load_plugin_from_info(info, root)
def _load_plugins(self):
plugins = {}
for search_folder in self._search_folders:
for root, dirnames, filenames in os.walk(search_folder):
for filename in fnmatch.filter(filenames, '*.senpy'):
name, plugin = self._load_plugin(root, filename)
if plugin and name:
plugins[name] = plugin
self._outdated = False
return plugins
def teardown(self, exception): def teardown(self, exception):
pass pass
@property
def plugins(self):
""" Return the plugins registered for a given application. """
if self._outdated:
self._plugin_list = self._load_plugins()
return self._plugin_list
def filter_plugins(self, **kwargs):
return plugins.pfilter(self.plugins, **kwargs)
@property
def analysis_plugins(self):
""" Return only the analysis plugins """
return self.filter_plugins(plugin_type='analysisPlugin')

View File

@@ -1,257 +0,0 @@
'''
Meta-programming for the models.
'''
import os
import json
import jsonschema
import inspect
import copy
from abc import ABCMeta
from collections import MutableMapping, namedtuple
class BaseMeta(ABCMeta):
'''
Metaclass for models. It extracts the default values for the fields in
the model.
For instance, instances of the following class wouldn't need to mark
their version or description on initialization:
.. code-block:: python
class MyPlugin(Plugin):
version=0.3
description='A dull plugin'
Note that these operations could be included in the __init__ of the
class, but it would be very inefficient.
'''
_subtypes = {}
def __new__(mcs, name, bases, attrs, **kwargs):
register_afterwards = False
defaults = {}
attrs = mcs.expand_with_schema(name, attrs)
if 'schema' in attrs:
register_afterwards = True
for base in bases:
if hasattr(base, '_defaults'):
defaults.update(getattr(base, '_defaults'))
info, rest = mcs.split_attrs(attrs)
for i in list(info.keys()):
if isinstance(info[i], _Alias):
fget, fset, fdel = make_property(info[i].indict)
rest[i] = property(fget=fget, fset=fset, fdel=fdel)
else:
defaults[i] = info[i]
rest['_defaults'] = defaults
cls = super(BaseMeta, mcs).__new__(mcs, name, tuple(bases), rest)
if register_afterwards:
mcs.register(cls, defaults['@type'])
return cls
@classmethod
def register(mcs, rsubclass, rtype=None):
mcs._subtypes[rtype or rsubclass.__name__] = rsubclass
@staticmethod
def expand_with_schema(name, attrs):
if 'schema' in attrs: # Schema specified by name
schema_file = '{}.json'.format(attrs['schema'])
elif 'schema_file' in attrs:
schema_file = attrs['schema_file']
del attrs['schema_file']
else:
return attrs
if '/' not in 'schema_file':
thisdir = os.path.dirname(os.path.realpath(__file__))
schema_file = os.path.join(thisdir,
'schemas',
schema_file)
schema_path = 'file://' + schema_file
with open(schema_file) as f:
schema = json.load(f)
resolver = jsonschema.RefResolver(schema_path, schema)
attrs['@type'] = "".join((name[0].lower(), name[1:]))
attrs['_schema_file'] = schema_file
attrs['schema'] = schema
attrs['_validator'] = jsonschema.Draft4Validator(schema, resolver=resolver)
schema_defaults = BaseMeta.get_defaults(attrs['schema'])
attrs.update(schema_defaults)
return attrs
@staticmethod
def is_func(v):
return inspect.isroutine(v) or inspect.ismethod(v) or \
inspect.ismodule(v) or isinstance(v, property)
@staticmethod
def is_internal(k):
return k[0] == '_' or k == 'schema' or k == 'data'
@staticmethod
def get_key(key):
if key[0] != '_':
key = key.replace("__", ":", 1)
return key
@staticmethod
def split_attrs(attrs):
'''
Extract the attributes of the class.
This allows adding default values in the class definition.
e.g.:
'''
isattr = {}
rest = {}
for key, value in attrs.items():
if not (BaseMeta.is_internal(key)) and (not BaseMeta.is_func(value)):
isattr[key] = value
else:
rest[key] = value
return isattr, rest
@staticmethod
def get_defaults(schema):
temp = {}
for obj in [
schema,
] + schema.get('allOf', []):
for k, v in obj.get('properties', {}).items():
if 'default' in v and k not in temp:
temp[k] = v['default']
return temp
def make_property(key):
def fget(self):
return self[key]
def fdel(self):
del self[key]
def fset(self, value):
self[key] = value
return fget, fset, fdel
class CustomDict(MutableMapping, object):
'''
A dictionary whose elements can also be accessed as attributes. Since some
characters are not valid in the dot-notation, the attribute names also
converted. e.g.:
> d = CustomDict()
> d.key = d['ns:name'] = 1
> d.key == d['key']
True
> d.ns__name == d['ns:name']
'''
_defaults = {}
_map_attr_key = {'id': '@id'}
def __init__(self, *args, **kwargs):
super(CustomDict, self).__init__()
for k, v in self._defaults.items():
self[k] = copy.copy(v)
for arg in args:
self.update(arg)
for k, v in kwargs.items():
self[self._attr_to_key(k)] = v
return self
def serializable(self):
def ser_or_down(item):
if hasattr(item, 'serializable'):
return item.serializable()
elif isinstance(item, dict):
temp = dict()
for kp in item:
vp = item[kp]
temp[kp] = ser_or_down(vp)
return temp
elif isinstance(item, list) or isinstance(item, set):
return list(ser_or_down(i) for i in item)
else:
return item
return ser_or_down(self.as_dict())
def __getitem__(self, key):
key = self._key_to_attr(key)
return self.__dict__[key]
def __setitem__(self, key, value):
'''Do not insert data directly, there might be a property in that key. '''
key = self._key_to_attr(key)
return setattr(self, key, value)
def as_dict(self):
return {self._attr_to_key(k): v for k, v in self.__dict__.items()
if not self._internal_key(k)}
def __iter__(self):
return (k for k in self.__dict__ if not self._internal_key(k))
def __len__(self):
return len(self.__dict__)
def __delitem__(self, key):
del self.__dict__[key]
def update(self, other):
for k, v in other.items():
self[k] = v
def _attr_to_key(self, key):
key = key.replace("__", ":", 1)
key = self._map_attr_key.get(key, key)
return key
def _key_to_attr(self, key):
if self._internal_key(key):
return key
key = key.replace(":", "__", 1)
return key
def __getattr__(self, key):
try:
return self.__dict__[self._attr_to_key(key)]
except KeyError:
raise AttributeError
@staticmethod
def _internal_key(key):
return key[0] == '_'
def __str__(self):
return str(self.serializable())
def __repr__(self):
return str(self.serializable())
_Alias = namedtuple('Alias', 'indict')
def alias(key):
return _Alias(key)

View File

@@ -6,29 +6,23 @@ For compatibility with Py3 and for easier debugging, this new version drops
introspection and adds all arguments to the models. introspection and adds all arguments to the models.
''' '''
from __future__ import print_function from __future__ import print_function
from future import standard_library from six import string_types
standard_library.install_aliases()
from future.utils import with_metaclass
from past.builtins import basestring
import time import time
import copy import copy
import json import json
import os import os
import jsonref import jsonref
import jsonschema
from flask import Response as FlaskResponse from flask import Response as FlaskResponse
from pyld import jsonld from pyld import jsonld
import logging
logging.getLogger('rdflib').setLevel(logging.WARN)
logger = logging.getLogger(__name__)
from rdflib import Graph from rdflib import Graph
import logging
from .meta import BaseMeta, CustomDict, alias logger = logging.getLogger(__name__)
DEFINITIONS_FILE = 'definitions.json' DEFINITIONS_FILE = 'definitions.json'
CONTEXT_PATH = os.path.join( CONTEXT_PATH = os.path.join(
@@ -51,100 +45,38 @@ def read_schema(schema_file, absolute=False):
return jsonref.load(f, base_uri=schema_uri) return jsonref.load(f, base_uri=schema_uri)
def dump_schema(schema): base_schema = read_schema(DEFINITIONS_FILE)
return jsonref.dumps(schema)
def load_context(context): class Context(dict):
logging.debug('Loading context: {}'.format(context)) @staticmethod
if not context: def load(context):
return context logging.debug('Loading context: {}'.format(context))
elif isinstance(context, list): if not context:
contexts = []
for c in context:
contexts.append(load_context(c))
return contexts
elif isinstance(context, dict):
return dict(context)
elif isinstance(context, basestring):
try:
with open(context) as f:
return dict(json.loads(f.read()))
except IOError:
return context return context
else: elif isinstance(context, list):
raise AttributeError('Please, provide a valid context') contexts = []
for c in context:
contexts.append(Context.load(c))
return contexts
elif isinstance(context, dict):
return Context(context)
elif isinstance(context, string_types):
try:
with open(context) as f:
return Context(json.loads(f.read()))
except IOError:
return context
else:
raise AttributeError('Please, provide a valid context')
base_context = load_context(CONTEXT_PATH) base_context = Context.load(CONTEXT_PATH)
def register(rsubclass, rtype=None): class SenpyMixin(object):
BaseMeta.register(rsubclass, rtype)
class BaseModel(with_metaclass(BaseMeta, CustomDict)):
'''
Entities of the base model are a special kind of dictionary that emulates
a JSON-LD object. The structure of the dictionary is checked via JSON-schema.
For convenience, the values can also be accessed as attributes
(a la Javascript). e.g.:
>>> myobject.key == myobject['key']
True
>>> myobject.ns__name == myobject['ns:name']
True
Additionally, subclasses of this class can specify default values for their
instances. These defaults are inherited by subclasses. e.g.:
>>> class NewModel(BaseModel):
... mydefault = 5
>>> n1 = NewModel()
>>> n1['mydefault'] == 5
True
>>> n1.mydefault = 3
>>> n1['mydefault'] = 3
True
>>> n2 = NewModel()
>>> n2 == 5
True
>>> class SubModel(NewModel):
pass
>>> subn = SubModel()
>>> subn.mydefault == 5
True
Lastly, every subclass that also specifies a schema will get registered, so it
is possible to deserialize JSON and get the right type.
i.e. to recover an instance of the original class from a plain JSON.
'''
schema_file = DEFINITIONS_FILE
_context = base_context["@context"] _context = base_context["@context"]
def __init__(self, *args, **kwargs):
auto_id = kwargs.pop('_auto_id', True)
super(BaseModel, self).__init__(*args, **kwargs)
if auto_id:
self.id
if '@type' not in self:
logger.warn('Created an instance of an unknown model')
@property
def id(self):
if '@id' not in self:
self['@id'] = ':{}_{}'.format(type(self).__name__, time.time())
return self['@id']
@id.setter
def id(self, value):
self['@id'] = value
def flask(self, def flask(self,
in_headers=True, in_headers=True,
headers=None, headers=None,
@@ -170,7 +102,7 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
}) })
return FlaskResponse( return FlaskResponse(
response=content, response=content,
status=self.get('status', 200), status=getattr(self, "status", 200),
headers=headers, headers=headers,
mimetype=mimetype) mimetype=mimetype)
@@ -198,57 +130,142 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
else: else:
return content return content
def serializable(self):
def ser_or_down(item):
if hasattr(item, 'serializable'):
return item.serializable()
elif isinstance(item, dict):
temp = dict()
for kp in item:
vp = item[kp]
temp[kp] = ser_or_down(vp)
return temp
elif isinstance(item, list):
return list(ser_or_down(i) for i in item)
else:
return item
return ser_or_down(self._plain_dict())
def jsonld(self, def jsonld(self,
with_context=False, with_context=True,
context_uri=None, context_uri=None,
prefix=None, prefix=None,
expanded=False): expanded=False):
ser = self.serializable()
result = self.serializable() result = jsonld.compact(
if context_uri or with_context: ser,
result['@context'] = context_uri or self._context self._context,
options={
# result = jsonld.compact(result, 'base': prefix,
# self._context, 'expandContext': self._context,
# options={ 'senpy': prefix
# 'base': prefix, })
# 'expandContext': self._context, if context_uri:
# 'senpy': prefix result['@context'] = context_uri
# })
if expanded: if expanded:
result = jsonld.expand( result = jsonld.expand(
result, options={'base': prefix, result, options={'base': prefix,
'expandContext': self._context}) 'expandContext': self._context})
if not with_context: if not with_context:
try: del result['@context']
del result['@context']
except KeyError:
pass
return result return result
def to_JSON(self, *args, **kwargs):
js = json.dumps(self.jsonld(*args, **kwargs), indent=4, sort_keys=True)
return js
def validate(self, obj=None): def validate(self, obj=None):
if not obj: if not obj:
obj = self obj = self
if hasattr(obj, "jsonld"): if hasattr(obj, "jsonld"):
obj = obj.jsonld() obj = obj.jsonld()
self._validator.validate(obj) jsonschema.validate(obj, self.schema)
def prov(self, another): def __str__(self):
self['prov:wasGeneratedBy'] = another.id return str(self.to_JSON())
def subtypes(): class BaseModel(SenpyMixin, dict):
return BaseMeta._subtypes
schema = base_schema
def __init__(self, *args, **kwargs):
if 'id' in kwargs:
self.id = kwargs.pop('id')
elif kwargs.pop('_auto_id', True):
self.id = '_:{}_{}'.format(type(self).__name__, time.time())
temp = dict(*args, **kwargs)
for obj in [
self.schema,
] + self.schema.get('allOf', []):
for k, v in obj.get('properties', {}).items():
if 'default' in v and k not in temp:
temp[k] = copy.deepcopy(v['default'])
for i in temp:
nk = self._get_key(i)
if nk != i:
temp[nk] = temp[i]
del temp[i]
try:
temp['@type'] = getattr(self, '@type')
except AttributeError:
logger.warn('Creating an instance of an unknown model')
super(BaseModel, self).__init__(temp)
def _get_key(self, key):
key = key.replace("__", ":", 1)
return key
def __setitem__(self, key, value):
dict.__setitem__(self, key, value)
def __delitem__(self, key):
dict.__delitem__(self, key)
def __getattr__(self, key):
try:
return self.__getitem__(self._get_key(key))
except KeyError:
raise AttributeError(key)
def __setattr__(self, key, value):
self.__setitem__(self._get_key(key), value)
def __delattr__(self, key):
try:
object.__delattr__(self, key)
except AttributeError:
self.__delitem__(self._get_key(key))
def _plain_dict(self):
d = {k: v for (k, v) in self.items() if k[0] != "_"}
if 'id' in d:
d["@id"] = d.pop('id')
return d
_subtypes = {}
def register(rsubclass, rtype=None):
_subtypes[rtype or rsubclass.__name__] = rsubclass
def from_dict(indict, cls=None): def from_dict(indict, cls=None):
if not cls: if not cls:
target = indict.get('@type', None) target = indict.get('@type', None)
cls = BaseModel
try: try:
cls = subtypes()[target] if target and target in _subtypes:
except KeyError: cls = _subtypes[target]
pass else:
cls = BaseModel
except Exception:
cls = BaseModel
outdict = dict() outdict = dict()
for k, v in indict.items(): for k, v in indict.items():
if k == '@context': if k == '@context':
@@ -256,11 +273,10 @@ def from_dict(indict, cls=None):
elif isinstance(v, dict): elif isinstance(v, dict):
v = from_dict(indict[k]) v = from_dict(indict[k])
elif isinstance(v, list): elif isinstance(v, list):
v = v[:]
for ix, v2 in enumerate(v): for ix, v2 in enumerate(v):
if isinstance(v2, dict): if isinstance(v2, dict):
v[ix] = from_dict(v2) v[ix] = from_dict(v2)
outdict[k] = copy.copy(v) outdict[k] = v
return cls(**outdict) return cls(**outdict)
@@ -273,63 +289,27 @@ def from_json(injson):
return from_dict(indict) return from_dict(indict)
class Entry(BaseModel): def from_schema(name, schema_file=None, base_classes=None):
schema = 'entry'
text = alias('nif:isString')
class Sentiment(BaseModel):
schema = 'sentiment'
polarity = alias('marl:hasPolarity')
polarityValue = alias('marl:hasPolarityValue')
class Error(BaseModel, Exception):
schema = 'error'
def __init__(self, message='Generic senpy exception', *args, **kwargs):
Exception.__init__(self, message)
super(Error, self).__init__(*args, **kwargs)
self.message = message
def __str__(self):
if not hasattr(self, 'errors'):
return self.message
return '{}:\n\t{}'.format(self.message, self.errors)
def __hash__(self):
return Exception.__hash__(self)
# Add the remaining schemas programmatically
def _class_from_schema(name, schema=None, schema_file=None, base_classes=None):
base_classes = base_classes or [] base_classes = base_classes or []
base_classes.append(BaseModel) base_classes.append(BaseModel)
attrs = {} schema_file = schema_file or '{}.json'.format(name)
if schema: class_name = '{}{}'.format(name[0].upper(), name[1:])
attrs['schema'] = schema newclass = type(class_name, tuple(base_classes), {})
elif schema_file: setattr(newclass, '@type', name)
attrs['schema_file'] = schema_file setattr(newclass, 'schema', read_schema(schema_file))
else: setattr(newclass, 'class_name', class_name)
attrs['schema'] = name register(newclass, name)
name = "".join((name[0].upper(), name[1:])) return newclass
return BaseMeta(name, base_classes, attrs)
def _add_class_from_schema(*args, **kwargs): def _add_from_schema(*args, **kwargs):
generatedClass = _class_from_schema(*args, **kwargs) generatedClass = from_schema(*args, **kwargs)
globals()[generatedClass.__name__] = generatedClass globals()[generatedClass.__name__] = generatedClass
del generatedClass del generatedClass
for i in [ for i in [
'aggregatedEvaluation',
'analysis', 'analysis',
'dataset',
'datasets',
'emotion', 'emotion',
'emotionConversion', 'emotionConversion',
'emotionConversionPlugin', 'emotionConversionPlugin',
@@ -337,17 +317,51 @@ for i in [
'emotionModel', 'emotionModel',
'emotionPlugin', 'emotionPlugin',
'emotionSet', 'emotionSet',
'evaluation', 'entry',
'entity',
'help',
'metric',
'plugin', 'plugin',
'plugins', 'plugins',
'response', 'response',
'results', 'results',
'sentiment',
'sentimentPlugin', 'sentimentPlugin',
'suggestion', 'suggestion',
'topic',
]: ]:
_add_class_from_schema(i) _add_from_schema(i)
_ErrorModel = from_schema('error')
class Error(SenpyMixin, Exception):
def __init__(self, message, *args, **kwargs):
super(Error, self).__init__(self, message, message)
self._error = _ErrorModel(message=message, *args, **kwargs)
self.message = message
def __getitem__(self, key):
return self._error[key]
def __setitem__(self, key, value):
self._error[key] = value
def __delitem__(self, key):
del self._error[key]
def __getattr__(self, key):
if key != '_error' and hasattr(self._error, key):
return getattr(self._error, key)
raise AttributeError(key)
def __setattr__(self, key, value):
if key != '_error':
return setattr(self._error, key, value)
else:
super(Error, self).__setattr__(key, value)
def __delattr__(self, key):
delattr(self._error, key)
def __str__(self):
return str(self.to_JSON(with_context=False))
register(Error, 'error')

View File

@@ -1,119 +1,32 @@
from future import standard_library from future import standard_library
standard_library.install_aliases() standard_library.install_aliases()
import inspect
from future.utils import with_metaclass
import os.path import os.path
import os import os
import re
import pickle import pickle
import logging import logging
import tempfile
import copy import copy
import pprint from .. import models
from ..api import API_PARAMS
import inspect
import sys
import subprocess
import importlib
import yaml
import threading
import nltk
from .. import models, utils
from .. import api
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
try:
from gsitk.evaluation.evaluation import Evaluation as Eval
from sklearn.pipeline import Pipeline
GSITK_AVAILABLE = True
except ImportError:
logger.warn('GSITK is not installed. Some functions will be unavailable.')
GSITK_AVAILABLE = False
class Plugin(models.Plugin):
class PluginMeta(models.BaseMeta): def __init__(self, info=None):
_classes = {}
def __new__(mcs, name, bases, attrs, **kwargs):
plugin_type = []
if hasattr(bases[0], 'plugin_type'):
plugin_type += bases[0].plugin_type
plugin_type.append(name)
alias = attrs.get('name', name)
attrs['plugin_type'] = plugin_type
attrs['name'] = alias
if 'description' not in attrs:
doc = attrs.get('__doc__', None)
if doc:
attrs['description'] = doc
else:
logger.warn(('Plugin {} does not have a description. '
'Please, add a short summary to help other developers').format(name))
cls = super(PluginMeta, mcs).__new__(mcs, name, bases, attrs)
if alias in mcs._classes:
if os.environ.get('SENPY_TESTING', ""):
raise Exception(('The type of plugin {} already exists. '
'Please, choose a different name').format(name))
else:
logger.warn('Overloading plugin class: {}'.format(alias))
mcs._classes[alias] = cls
return cls
@classmethod
def for_type(cls, ptype):
return cls._classes[ptype]
class Plugin(with_metaclass(PluginMeta, models.Plugin)):
'''
Base class for all plugins in senpy.
A plugin must provide at least these attributes:
- version
- description (or docstring)
- author
Additionally, they may provide a URL (url) of a repository or website.
'''
def __init__(self, info=None, data_folder=None, **kwargs):
""" """
Provides a canonical name for plugins and serves as base for other Provides a canonical name for plugins and serves as base for other
kinds of plugins. kinds of plugins.
""" """
if not info:
raise models.Error(message=("You need to provide configuration"
"information for the plugin."))
logger.debug("Initialising {}".format(info)) logger.debug("Initialising {}".format(info))
super(Plugin, self).__init__(**kwargs) id = 'plugins/{}_{}'.format(info['name'], info['version'])
if info: super(Plugin, self).__init__(id=id, **info)
self.update(info)
self.validate()
self.id = 'plugins/{}_{}'.format(self['name'], self['version'])
self.is_activated = False self.is_activated = False
self._lock = threading.Lock()
self.data_folder = data_folder or os.getcwd()
self._directory = os.path.abspath(os.path.dirname(inspect.getfile(self.__class__)))
self._data_paths = ['',
self._directory,
os.path.join(self._directory, 'data'),
self.data_folder]
self._log = logging.getLogger('{}.{}'.format(__name__, self.name))
@property
def log(self):
return self._log
def validate(self):
missing = []
for x in ['name', 'description', 'version']:
if x not in self:
missing.append(x)
if missing:
raise models.Error('Missing configuration parameters: {}'.format(missing))
def get_folder(self): def get_folder(self):
return os.path.dirname(inspect.getfile(self.__class__)) return os.path.dirname(inspect.getfile(self.__class__))
@@ -124,74 +37,15 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
def deactivate(self): def deactivate(self):
pass pass
def test(self, test_cases=None):
if not test_cases:
if not hasattr(self, 'test_cases'):
raise AttributeError(('Plugin {} [{}] does not have any defined '
'test cases').format(self.id,
inspect.getfile(self.__class__)))
test_cases = self.test_cases
for case in test_cases:
try:
self.test_case(case)
self.log.debug('Test case passed:\n{}'.format(pprint.pformat(case)))
except Exception as ex:
self.log.warn('Test case failed:\n{}'.format(pprint.pformat(case)))
raise
def test_case(self, case):
entry = models.Entry(case['entry'])
given_parameters = case.get('params', case.get('parameters', {}))
expected = case.get('expected', None)
should_fail = case.get('should_fail', False)
try:
params = api.parse_params(given_parameters, self.extra_params)
res = list(self.analyse_entries([entry, ], params))
if not isinstance(expected, list):
expected = [expected]
utils.check_template(res, expected)
for r in res:
r.validate()
except models.Error:
if should_fail:
return
raise
assert not should_fail
def find_file(self, fname):
for p in self._data_paths:
alternative = os.path.join(p, fname)
if os.path.exists(alternative):
return alternative
raise IOError('File does not exist: {}'.format(fname))
def open(self, fpath, mode='r'):
if 'w' in mode:
# When writing, only use absolute paths or data_folder
if not os.path.isabs(fpath):
fpath = os.path.join(self.data_folder, fpath)
else:
fpath = self.find_file(fpath)
return open(fpath, mode=mode)
def serve(self, debug=True, **kwargs):
utils.easy(plugin_list=[self, ], plugin_folder=None, debug=debug, **kwargs)
# For backwards compatibility
SenpyPlugin = Plugin SenpyPlugin = Plugin
class Analysis(Plugin): class AnalysisPlugin(Plugin):
'''
A subclass of Plugin that analyses text and provides an annotation.
'''
def analyse(self, *args, **kwargs): def analyse(self, *args, **kwargs):
raise NotImplementedError( raise NotImplemented(
'Your plugin should implement either analyse or analyse_entry') 'Your method should implement either analyse or analyse_entry')
def analyse_entry(self, entry, parameters): def analyse_entry(self, entry, parameters):
""" An implemented plugin should override this method. """ An implemented plugin should override this method.
@@ -200,7 +54,7 @@ class Analysis(Plugin):
Note that this method may yield an annotated entry or a list of Note that this method may yield an annotated entry or a list of
entries (e.g. in a tokenizer) entries (e.g. in a tokenizer)
""" """
text = entry['nif:isString'] text = entry['text']
params = copy.copy(parameters) params = copy.copy(parameters)
params['input'] = text params['input'] = text
results = self.analyse(**params) results = self.analyse(**params)
@@ -209,196 +63,43 @@ class Analysis(Plugin):
def analyse_entries(self, entries, parameters): def analyse_entries(self, entries, parameters):
for entry in entries: for entry in entries:
self.log.debug('Analysing entry with plugin {}: {}'.format(self, entry)) logger.debug('Analysing entry with plugin {}: {}'.format(self, entry))
results = self.analyse_entry(entry, parameters) for result in self.analyse_entry(entry, parameters):
if inspect.isgenerator(results): yield result
for result in results:
yield result
else:
yield results
def test_case(self, case):
if 'entry' not in case and 'input' in case:
entry = models.Entry(_auto_id=False)
entry.nif__isString = case['input']
case['entry'] = entry
super(Analysis, self).test_case(case)
AnalysisPlugin = Analysis class ConversionPlugin(Plugin):
class Conversion(Plugin):
'''
A subclass of Plugins that convert between different annotation models.
e.g. a conversion of emotion models, or normalization of sentiment values.
'''
pass pass
ConversionPlugin = Conversion class SentimentPlugin(models.SentimentPlugin, AnalysisPlugin):
def __init__(self, info, *args, **kwargs):
super(SentimentPlugin, self).__init__(info, *args, **kwargs)
self.minPolarityValue = float(info.get("minPolarityValue", 0))
self.maxPolarityValue = float(info.get("maxPolarityValue", 1))
class SentimentPlugin(Analysis, models.SentimentPlugin): class EmotionPlugin(models.EmotionPlugin, AnalysisPlugin):
''' def __init__(self, info, *args, **kwargs):
Sentiment plugins provide sentiment annotation (using Marl) super(EmotionPlugin, self).__init__(info, *args, **kwargs)
''' self.minEmotionValue = float(info.get("minEmotionValue", -1))
minPolarityValue = 0 self.maxEmotionValue = float(info.get("maxEmotionValue", 1))
maxPolarityValue = 1
def test_case(self, case):
if 'polarity' in case:
expected = case.get('expected', {})
s = models.Sentiment(_auto_id=False)
s.marl__hasPolarity = case['polarity']
if 'sentiments' not in expected:
expected['sentiments'] = []
expected['sentiments'].append(s)
case['expected'] = expected
super(SentimentPlugin, self).test_case(case)
class EmotionPlugin(Analysis, models.EmotionPlugin): class EmotionConversionPlugin(models.EmotionConversionPlugin, ConversionPlugin):
'''
Emotion plugins provide emotion annotation (using Onyx)
'''
minEmotionValue = 0
maxEmotionValue = 1
class EmotionConversion(Conversion):
'''
A subclass of Conversion that converts emotion annotations using different models
'''
pass pass
EmotionConversionPlugin = EmotionConversion
class Box(AnalysisPlugin):
'''
Black box plugins delegate analysis to a function.
The flow is like so:
.. code-block::
entry --> input() --> predict_one() --> output() --> entry'
In other words: their ``input`` method convers a query (entry and a set of parameters) into
the input to the box method. The ``output`` method convers the results given by the box into
an entry that senpy can handle.
'''
def input(self, entry, params=None):
'''Transforms a query (entry+param) into an input for the black box'''
return entry
def output(self, output, entry=None, params=None):
'''Transforms the results of the black box into an entry'''
return output
def predict_one(self, input):
raise NotImplementedError('You should define the behavior of this plugin')
def analyse_entries(self, entries, params):
for entry in entries:
input = self.input(entry=entry, params=params)
results = self.predict_one(input=input)
yield self.output(output=results, entry=entry, params=params)
def fit(self, X=None, y=None):
return self
def transform(self, X):
return [self.predict_one(x) for x in X]
def predict(self, X):
return self.transform(X)
def fit_transform(self, X, y):
self.fit(X, y)
return self.transform(X)
def as_pipe(self):
pipe = Pipeline([('plugin', self)])
pipe.name = self.name
return pipe
class TextBox(Box):
'''A black box plugin that takes only text as input'''
def input(self, entry, params):
entry = super(TextBox, self).input(entry, params)
return entry['nif:isString']
class SentimentBox(TextBox, SentimentPlugin):
'''
A box plugin where the output is only a polarity label or a tuple (polarity, polarityValue)
'''
def output(self, output, entry, **kwargs):
s = models.Sentiment()
try:
label, value = output
except ValueError:
label, value = output, None
s.prov(self)
s.polarity = label
if value is not None:
s.polarityValue = value
entry.sentiments.append(s)
return entry
class EmotionBox(TextBox, EmotionPlugin):
'''
A box plugin where the output is only an a tuple of emotion labels
'''
def output(self, output, entry, **kwargs):
if not isinstance(output, list):
output = [output]
s = models.EmotionSet()
entry.emotions.append(s)
for label in output:
e = models.Emotion(onyx__hasEmotionCategory=label)
s.append(e)
return entry
class MappingMixin(object):
@property
def mappings(self):
return self._mappings
@mappings.setter
def mappings(self, value):
self._mappings = value
def output(self, output, entry, params):
output = self.mappings.get(output,
self.mappings.get('default', output))
return super(MappingMixin, self).output(output=output,
entry=entry,
params=params)
class ShelfMixin(object): class ShelfMixin(object):
@property @property
def sh(self): def sh(self):
if not hasattr(self, '_sh') or self._sh is None: if not hasattr(self, '_sh') or self._sh is None:
self._sh = {} self.__dict__['_sh'] = {}
if os.path.isfile(self.shelf_file): if os.path.isfile(self.shelf_file):
try: try:
with self.open(self.shelf_file, 'rb') as p: self.__dict__['_sh'] = pickle.load(open(self.shelf_file, 'rb'))
self._sh = pickle.load(p)
except (IndexError, EOFError, pickle.UnpicklingError): except (IndexError, EOFError, pickle.UnpicklingError):
self.log.warning('Corrupted shelf file: {}'.format(self.shelf_file)) logger.warning('{} has a corrupted shelf file!'.format(self.id))
if not self.get('force_shelf', False): if not self.get('force_shelf', False):
raise raise
return self._sh return self._sh
@@ -407,49 +108,44 @@ class ShelfMixin(object):
def sh(self): def sh(self):
if os.path.isfile(self.shelf_file): if os.path.isfile(self.shelf_file):
os.remove(self.shelf_file) os.remove(self.shelf_file)
del self._sh del self.__dict__['_sh']
self.save() self.save()
@sh.setter
def sh(self, value):
self._sh = value
@property @property
def shelf_file(self): def shelf_file(self):
if not hasattr(self, '_shelf_file') or not self._shelf_file: if 'shelf_file' not in self or not self['shelf_file']:
self._shelf_file = os.path.join(self.data_folder, self.name + '.p') sd = os.environ.get('SENPY_DATA', tempfile.gettempdir())
return self._shelf_file self.shelf_file = os.path.join(sd, self.name + '.p')
return self['shelf_file']
@shelf_file.setter
def shelf_file(self, value):
self._shelf_file = value
def save(self): def save(self):
self.log.debug('Saving pickle') logger.debug('saving pickle')
if hasattr(self, '_sh') and self._sh is not None: if hasattr(self, '_sh') and self._sh is not None:
with self.open(self.shelf_file, 'wb') as f: with open(self.shelf_file, 'wb') as f:
pickle.dump(self._sh, f) pickle.dump(self._sh, f)
def pfilter(plugins, plugin_type=Analysis, **kwargs): default_plugin_type = API_PARAMS['plugin_type']['default']
def pfilter(plugins, **kwargs):
""" Filter plugins by different criteria """ """ Filter plugins by different criteria """
if isinstance(plugins, models.Plugins): if isinstance(plugins, models.Plugins):
plugins = plugins.plugins plugins = plugins.plugins
elif isinstance(plugins, dict): elif isinstance(plugins, dict):
plugins = plugins.values() plugins = plugins.values()
ptype = kwargs.pop('plugin_type', default_plugin_type)
logger.debug('#' * 100) logger.debug('#' * 100)
logger.debug('plugin_type {}'.format(plugin_type)) logger.debug('ptype {}'.format(ptype))
if plugin_type: if ptype:
if isinstance(plugin_type, PluginMeta):
plugin_type = plugin_type.__name__
try: try:
plugin_type = plugin_type[0].upper() + plugin_type[1:] ptype = ptype[0].upper() + ptype[1:]
pclass = globals()[plugin_type] pclass = globals()[ptype]
logger.debug('Class: {}'.format(pclass)) logger.debug('Class: {}'.format(pclass))
candidates = filter(lambda x: isinstance(x, pclass), candidates = filter(lambda x: isinstance(x, pclass),
plugins) plugins)
except KeyError: except KeyError:
raise models.Error('{} is not a valid type'.format(plugin_type)) raise models.Error('{} is not a valid type'.format(ptype))
else: else:
candidates = plugins candidates = plugins
@@ -463,202 +159,4 @@ def pfilter(plugins, plugin_type=Analysis, **kwargs):
if kwargs: if kwargs:
candidates = filter(matches, candidates) candidates = filter(matches, candidates)
return candidates return {p.name: p for p in candidates}
def load_module(name, root=None):
if root:
sys.path.append(root)
tmp = importlib.import_module(name)
if root:
sys.path.remove(root)
return tmp
def _log_subprocess_output(process):
for line in iter(process.stdout.readline, b''):
logger.info('%r', line)
for line in iter(process.stderr.readline, b''):
logger.error('%r', line)
def install_deps(*plugins):
installed = False
nltk_resources = set()
for info in plugins:
requirements = info.get('requirements', [])
if requirements:
pip_args = [sys.executable, '-m', 'pip', 'install']
for req in requirements:
pip_args.append(req)
logger.info('Installing requirements: ' + str(requirements))
process = subprocess.Popen(pip_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
_log_subprocess_output(process)
exitcode = process.wait()
installed = True
if exitcode != 0:
raise models.Error("Dependencies not properly installed")
nltk_resources |= set(info.get('nltk_resources', []))
installed |= nltk.download(list(nltk_resources))
return installed
is_plugin_file = re.compile(r'.*\.senpy$|senpy_[a-zA-Z0-9_]+\.py$|'
'^(?!test_)[a-zA-Z0-9_]+_plugin.py$')
def find_plugins(folders):
for search_folder in folders:
for root, dirnames, filenames in os.walk(search_folder):
# Do not look for plugins in hidden or special folders
dirnames[:] = [d for d in dirnames if d[0] not in ['.', '_']]
for filename in filter(is_plugin_file.match, filenames):
fpath = os.path.join(root, filename)
yield fpath
def from_path(fpath, **kwargs):
logger.debug("Loading plugin from {}".format(fpath))
if fpath.endswith('.py'):
# We asume root is the dir of the file, and module is the name of the file
root = os.path.dirname(fpath)
module = os.path.basename(fpath)[:-3]
for instance in _from_module_name(module=module, root=root, **kwargs):
yield instance
else:
info = parse_plugin_info(fpath)
yield from_info(info, **kwargs)
def from_folder(folders, loader=from_path, **kwargs):
plugins = []
for fpath in find_plugins(folders):
for plugin in loader(fpath, **kwargs):
plugins.append(plugin)
return plugins
def from_info(info, root=None, **kwargs):
if any(x not in info for x in ('module',)):
raise ValueError('Plugin info is not valid: {}'.format(info))
module = info["module"]
if not root and '_path' in info:
root = os.path.dirname(info['_path'])
return one_from_module(module, root=root, info=info, **kwargs)
def parse_plugin_info(fpath):
logger.debug("Parsing plugin info: {}".format(fpath))
with open(fpath, 'r') as f:
info = yaml.load(f)
info['_path'] = fpath
return info
def from_module(module, **kwargs):
if inspect.ismodule(module):
res = _from_loaded_module(module, **kwargs)
else:
res = _from_module_name(module, **kwargs)
for p in res:
yield p
def one_from_module(module, root, info, **kwargs):
if '@type' in info:
cls = PluginMeta.from_type(info['@type'])
return cls(info=info, **kwargs)
instance = next(from_module(module=module, root=root, info=info, **kwargs), None)
if not instance:
raise Exception("No valid plugin for: {}".format(module))
return instance
def _classes_in_module(module):
for _, obj in inspect.getmembers(module):
if inspect.isclass(obj) and inspect.getmodule(obj) == module:
logger.debug(("Found plugin class:"
" {}@{}").format(obj, inspect.getmodule(obj)))
yield obj
def _instances_in_module(module):
for _, obj in inspect.getmembers(module):
if isinstance(obj, Plugin) and inspect.getmodule(obj) == module:
logger.debug(("Found plugin instance:"
" {}@{}").format(obj, inspect.getmodule(obj)))
yield obj
def _from_module_name(module, root, info=None, install=True, **kwargs):
try:
module = load_module(module, root)
except (ImportError, LookupError):
if not install or not info:
raise
install_deps(info)
module = load_module(module, root)
for plugin in _from_loaded_module(module=module, root=root, info=info, **kwargs):
if install:
install_deps(plugin)
yield plugin
def _from_loaded_module(module, info=None, **kwargs):
for cls in _classes_in_module(module):
yield cls(info=info, **kwargs)
for instance in _instances_in_module(module):
yield instance
def evaluate(plugins, datasets, **kwargs):
if not GSITK_AVAILABLE:
raise Exception('GSITK is not available. Install it to use this function.')
ev = Eval(tuples=None,
datasets=datasets,
pipelines=[plugin.as_pipe() for plugin in plugins])
ev.evaluate()
results = ev.results
evaluations = evaluations_to_JSONLD(results, **kwargs)
return evaluations
def evaluations_to_JSONLD(results, flatten=False):
'''
Map the evaluation results to a JSONLD scheme
'''
evaluations = list()
metric_names = ['accuracy', 'precision_macro', 'recall_macro',
'f1_macro', 'f1_weighted', 'f1_micro', 'f1_macro']
for index, row in results.iterrows():
evaluation = models.Evaluation()
if row.get('CV', True):
evaluation['@type'] = ['StaticCV', 'Evaluation']
evaluation.evaluatesOn = row['Dataset']
evaluation.evaluates = row['Model']
i = 0
if flatten:
metric = models.Metric()
for name in metric_names:
metric[name] = row[name]
evaluation.metrics.append(metric)
else:
# We should probably discontinue this representation
for name in metric_names:
metric = models.Metric()
metric['@id'] = 'Metric' + str(i)
metric['@type'] = name.capitalize()
metric.value = row[name]
evaluation.metrics.append(metric)
i += 1
evaluations.append(evaluation)
return evaluations

View File

@@ -6,12 +6,7 @@ logger = logging.getLogger(__name__)
class CentroidConversion(EmotionConversionPlugin): class CentroidConversion(EmotionConversionPlugin):
''' def __init__(self, info):
This plugin converts emotion annotations from a dimensional model to a
categorical one, and vice versa. The centroids used in the conversion
are configurable and appear in the semantic description of the plugin.
'''
def __init__(self, info, *args, **kwargs):
if 'centroids' not in info: if 'centroids' not in info:
raise Error('Centroid conversion plugins should provide ' raise Error('Centroid conversion plugins should provide '
'the centroids in their senpy file') 'the centroids in their senpy file')
@@ -38,7 +33,7 @@ class CentroidConversion(EmotionConversionPlugin):
ncentroids[aliases.get(k1, k1)] = nv1 ncentroids[aliases.get(k1, k1)] = nv1
info['centroids'] = ncentroids info['centroids'] = ncentroids
super(CentroidConversion, self).__init__(info, *args, **kwargs) super(CentroidConversion, self).__init__(info)
self.dimensions = set() self.dimensions = set()
for c in self.centroids.values(): for c in self.centroids.values():
@@ -105,54 +100,3 @@ class CentroidConversion(EmotionConversionPlugin):
else: else:
raise Error('EMOTION MODEL NOT KNOWN') raise Error('EMOTION MODEL NOT KNOWN')
yield e yield e
def test(self, info=None):
if not info:
info = {
"name": "CentroidTest",
"description": "Centroid test",
"version": 0,
"centroids": {
"c1": {"V1": 0.5,
"V2": 0.5},
"c2": {"V1": -0.5,
"V2": 0.5},
"c3": {"V1": -0.5,
"V2": -0.5},
"c4": {"V1": 0.5,
"V2": -0.5}},
"aliases": {
"V1": "X-dimension",
"V2": "Y-dimension"
},
"centroids_direction": ["emoml:big6", "emoml:fsre-dimensions"]
}
c = CentroidConversion(info)
es1 = EmotionSet()
e1 = Emotion()
e1.onyx__hasEmotionCategory = "c1"
es1.onyx__hasEmotion.append(e1)
res = c._forward_conversion(es1)
assert res["X-dimension"] == 0.5
assert res["Y-dimension"] == 0.5
e2 = Emotion()
e2.onyx__hasEmotionCategory = "c2"
es1.onyx__hasEmotion.append(e2)
res = c._forward_conversion(es1)
assert res["X-dimension"] == 0
assert res["Y-dimension"] == 1
e = Emotion()
e["X-dimension"] = -0.2
e["Y-dimension"] = -0.3
res = c._backwards_conversion(e)
assert res["onyx:hasEmotionCategory"] == "c3"
e = Emotion()
e["X-dimension"] = -0.2
e["Y-dimension"] = 0.3
res = c._backwards_conversion(e)
assert res["onyx:hasEmotionCategory"] == "c2"

View File

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

View File

@@ -2,9 +2,13 @@
name: Ekman2PAD name: Ekman2PAD
module: senpy.plugins.conversion.emotion.centroids module: senpy.plugins.conversion.emotion.centroids
description: Plugin to convert emotion sets from Ekman to VAD description: Plugin to convert emotion sets from Ekman to VAD
version: 0.2 version: 0.1
# No need to specify onyx:doesConversion because centroids.py adds it automatically from centroids_direction # No need to specify onyx:doesConversion because centroids.py adds it automatically from centroids_direction
neutralValue: 5.0 origin:
# Point in VAD space with no emotion (aka Neutral)
A: 5.0
D: 5.0
V: 5.0
centroids: centroids:
anger: anger:
A: 6.95 A: 6.95
@@ -30,9 +34,9 @@ centroids_direction:
- emoml:big6 - emoml:big6
- emoml:pad - emoml:pad
aliases: # These are aliases for any key in the centroid, to avoid repeating a long name several times aliases: # These are aliases for any key in the centroid, to avoid repeating a long name several times
A: emoml:pad-dimensions:arousal A: emoml:arousal
V: emoml:pad-dimensions:pleasure V: emoml:valence
D: emoml:pad-dimensions:dominance D: emoml:dominance
anger: emoml:big6anger anger: emoml:big6anger
disgust: emoml:big6disgust disgust: emoml:big6disgust
fear: emoml:big6fear fear: emoml:big6fear

View File

@@ -1,18 +1,10 @@
import random import random
from senpy.plugins import EmotionPlugin from senpy.plugins import EmotionPlugin
from senpy.models import EmotionSet, Emotion, Entry from senpy.models import EmotionSet, Emotion
class EmoRand(EmotionPlugin): class RmoRandPlugin(EmotionPlugin):
name = "emoRand"
description = 'A sample plugin that returns a random emotion annotation'
author = '@balkian'
version = '0.1'
url = "https://github.com/gsi-upm/senpy-plugins-community"
requirements = {}
onyx__usesEmotionModel = "emoml:big6"
def analyse_entry(self, entry, params): def analyse_entry(self, entry, params):
category = "emoml:big6happiness" category = "emoml:big6happiness"
number = max(-1, min(1, random.gauss(0, 0.5))) number = max(-1, min(1, random.gauss(0, 0.5)))
@@ -24,11 +16,3 @@ class EmoRand(EmotionPlugin):
emotionSet.prov__wasGeneratedBy = self.id emotionSet.prov__wasGeneratedBy = self.id
entry.emotions.append(emotionSet) entry.emotions.append(emotionSet)
yield entry yield entry
def test(self):
params = dict()
results = list()
for i in range(100):
res = next(self.analyse_entry(Entry(nif__isString="Hello"), params))
res.validate()
results.append(res.emotions[0]['onyx:hasEmotion'][0]['onyx:hasEmotionCategory'])

View File

@@ -0,0 +1,9 @@
---
name: emoRand
module: emoRand
description: A sample plugin that returns a random emotion annotation
author: "@balkian"
version: '0.1'
url: "https://github.com/gsi-upm/senpy-plugins-community"
requirements: {}
onyx:usesEmotionModel: "emoml:big6"

View File

@@ -1,32 +0,0 @@
import random
from senpy.plugins import EmotionPlugin
from senpy.models import EmotionSet, Emotion, Entry
class EmoRand(EmotionPlugin):
'''A sample plugin that returns a random emotion annotation'''
author = '@balkian'
version = '0.1'
url = "https://github.com/gsi-upm/senpy-plugins-community"
onyx__usesEmotionModel = "emoml:big6"
def analyse_entry(self, entry, params):
category = "emoml:big6happiness"
number = max(-1, min(1, random.gauss(0, 0.5)))
if number > 0:
category = "emoml:big6anger"
emotionSet = EmotionSet()
emotion = Emotion({"onyx:hasEmotionCategory": category})
emotionSet.onyx__hasEmotion.append(emotion)
emotionSet.prov__wasGeneratedBy = self.id
entry.emotions.append(emotionSet)
yield entry
def test(self):
params = dict()
results = list()
for i in range(100):
res = next(self.analyse_entry(Entry(nif__isString="Hello"), params))
res.validate()
results.append(res.emotions[0]['onyx:hasEmotion'][0]['onyx:hasEmotionCategory'])

View File

@@ -0,0 +1,24 @@
import random
from senpy.plugins import SentimentPlugin
from senpy.models import Sentiment
class RandPlugin(SentimentPlugin):
def analyse_entry(self, entry, params):
lang = params.get("language", "auto")
polarity_value = max(-1, min(1, random.gauss(0.2, 0.2)))
polarity = "marl:Neutral"
if polarity_value > 0:
polarity = "marl:Positive"
elif polarity_value < 0:
polarity = "marl:Negative"
sentiment = Sentiment({
"marl:hasPolarity": polarity,
"marl:polarityValue": polarity_value
})
sentiment["prov:wasGeneratedBy"] = self.id
entry.sentiments.append(sentiment)
entry.language = lang
yield entry

View File

@@ -0,0 +1,10 @@
---
name: rand
module: rand
description: A sample plugin that returns a random sentiment annotation
author: "@balkian"
version: '0.1'
url: "https://github.com/gsi-upm/senpy-plugins-community"
requirements: {}
marl:maxPolarityValue: '1'
marl:minPolarityValue: "-1"

View File

@@ -1,36 +0,0 @@
import random
from senpy import SentimentPlugin, Sentiment, Entry
class Rand(SentimentPlugin):
'''A sample plugin that returns a random sentiment annotation'''
author = "@balkian"
version = '0.1'
url = "https://github.com/gsi-upm/senpy-plugins-community"
marl__maxPolarityValue = '1'
marl__minPolarityValue = "-1"
def analyse_entry(self, entry, params):
polarity_value = max(-1, min(1, random.gauss(0.2, 0.2)))
polarity = "marl:Neutral"
if polarity_value > 0:
polarity = "marl:Positive"
elif polarity_value < 0:
polarity = "marl:Negative"
sentiment = Sentiment(marl__hasPolarity=polarity,
marl__polarityValue=polarity_value)
sentiment.prov(self)
entry.sentiments.append(sentiment)
yield entry
def test(self):
'''Run several random analyses.'''
params = dict()
results = list()
for i in range(50):
res = next(self.analyse_entry(Entry(nif__isString="Hello"),
params))
res.validate()
results.append(res.sentiments[0]['marl:hasPolarity'])
assert 'marl:Positive' in results
assert 'marl:Negative' in results

View File

@@ -1,66 +0,0 @@
from senpy.plugins import AnalysisPlugin
from senpy.models import Entry
from nltk.tokenize.punkt import PunktSentenceTokenizer
from nltk.tokenize.simple import LineTokenizer
import nltk
class SplitPlugin(AnalysisPlugin):
'''description: A sample plugin that chunks input text'''
def activate(self):
nltk.download('punkt')
def analyse_entry(self, entry, params):
chunker_type = params["delimiter"]
original_text = entry['nif:isString']
if chunker_type == "sentence":
tokenizer = PunktSentenceTokenizer()
if chunker_type == "paragraph":
tokenizer = LineTokenizer()
chars = list(tokenizer.span_tokenize(original_text))
for i, chunk in enumerate(tokenizer.tokenize(original_text)):
print(chunk)
e = Entry()
e['nif:isString'] = chunk
if entry.id:
e.id = entry.id + "#char={},{}".format(chars[i][0], chars[i][1])
yield e
test_cases = [
{
'entry': {
'nif:isString': 'Hello. World.'
},
'params': {
'delimiter': 'sentence',
},
'expected': [
{
'nif:isString': 'Hello.'
},
{
'nif:isString': 'World.'
}
]
},
{
'entry': {
"@id": ":test",
'nif:isString': 'Hello\nWorld'
},
'params': {
'delimiter': 'paragraph',
},
'expected': [
{
"@id": ":test#char=0,5",
'nif:isString': 'Hello'
},
{
"@id": ":test#char=6,11",
'nif:isString': 'World'
}
]
}
]

View File

@@ -6,14 +6,13 @@ from senpy.models import Sentiment
class Sentiment140Plugin(SentimentPlugin): class Sentiment140Plugin(SentimentPlugin):
'''Connects to the sentiment140 free API: http://sentiment140.com'''
def analyse_entry(self, entry, params): def analyse_entry(self, entry, params):
lang = params["language"] lang = params.get("language", "auto")
res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson", res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson",
json.dumps({ json.dumps({
"language": lang, "language": lang,
"data": [{ "data": [{
"text": entry['nif:isString'] "text": entry.text
}] }]
})) }))
p = params.get("prefix", None) p = params.get("prefix", None)
@@ -35,32 +34,3 @@ class Sentiment140Plugin(SentimentPlugin):
entry.sentiments.append(sentiment) entry.sentiments.append(sentiment)
entry.language = lang entry.language = lang
yield entry yield entry
def test(self, *args, **kwargs):
'''
To avoid calling the sentiment140 API, we will mock the results
from requests.
'''
from senpy.test import patch_requests
expected = {"data": [{"polarity": 4}]}
with patch_requests(expected) as (request, response):
super(Sentiment140Plugin, self).test(*args, **kwargs)
assert request.called
assert response.json.called
test_cases = [
{
'entry': {
'nif:isString': 'I love Titanic'
},
'params': {},
'expected': {
"nif:isString": "I love Titanic",
'sentiments': [
{
'marl:hasPolarity': 'marl:Positive',
}
]
}
}
]

View File

@@ -16,7 +16,6 @@ extra_params:
- es - es
- en - en
- auto - auto
default: auto
requirements: {} requirements: {}
maxPolarityValue: 1 maxPolarityValue: 1
minPolarityValue: 0 minPolarityValue: 0

View File

@@ -0,0 +1,30 @@
from senpy.plugins import AnalysisPlugin
from senpy.models import Entry
from nltk.tokenize.punkt import PunktSentenceTokenizer
from nltk.tokenize.simple import LineTokenizer
import nltk
class SplitPlugin(AnalysisPlugin):
def activate(self):
nltk.download('punkt')
def analyse_entry(self, entry, params):
chunker_type = params.get("delimiter", "sentence")
original_id = entry.id
original_text = entry.get("text", None)
if chunker_type == "sentence":
tokenizer = PunktSentenceTokenizer()
chars = tokenizer.span_tokenize(original_text)
for i, sentence in enumerate(tokenizer.tokenize(original_text)):
e = Entry()
e.text = sentence
e.id = original_id + "#char={},{}".format(chars[i][0], chars[i][1])
yield e
if chunker_type == "paragraph":
tokenizer = LineTokenizer()
chars = tokenizer.span_tokenize(original_text)
for i, paragraph in enumerate(tokenizer.tokenize(original_text)):
e = Entry()
e.text = paragraph
chars = [char for char in chars]
e.id = original_id + "#char={},{}".format(chars[i][0], chars[i][1])
yield e

View File

@@ -1,12 +1,11 @@
--- ---
name: split name: split
module: senpy.plugins.misc.split module: split
description: A sample plugin that chunks input text description: A sample plugin that chunks input text
author: "@militarpancho" author: "@militarpancho"
version: '0.2' version: '0.1'
url: "https://github.com/gsi-upm/senpy" url: "https://github.com/gsi-upm/senpy"
requirements: requirements: {nltk}
- nltk
extra_params: extra_params:
delimiter: delimiter:
aliases: aliases:

View File

@@ -1,38 +0,0 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"allOf": [
{"$ref": "response.json"},
{
"title": "AggregatedEvaluation",
"description": "The results of the evaluation",
"type": "object",
"properties": {
"@context": {
"$ref": "context.json"
},
"@type": {
"default": "AggregatedEvaluation"
},
"@id": {
"description": "ID of the aggregated evaluation",
"type": "string"
},
"evaluations": {
"default": [],
"type": "array",
"items": {
"anyOf": [
{
"$ref": "evaluation.json"
},{
"type": "string"
}
]
}
}
},
"required": ["@id", "evaluations"]
}
]
}

View File

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

View File

@@ -1,29 +0,0 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"name": "Dataset",
"properties": {
"@id": {
"type": "string"
},
"name": {
"type": "string"
},
"compression": {
"type": "string"
},
"expected_bytes": {
"type": "int"
},
"filename": {
"description": "Name of the dataset",
"type": "string"
},
"url": {
"description": "Classifier or plugin evaluated",
"type": "string"
},
"stats": {
}
},
"required": ["@id"]
}

View File

@@ -1,18 +0,0 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"allOf": [
{"$ref": "response.json"},
{
"required": ["datasets"],
"properties": {
"datasets": {
"type": "array",
"default": [],
"items": {
"$ref": "dataset.json"
}
}
}
}
]
}

View File

@@ -41,20 +41,5 @@
}, },
"Response": { "Response": {
"$ref": "response.json" "$ref": "response.json"
},
"AggregatedEvaluation": {
"$ref": "aggregatedEvaluation.json"
},
"Evaluation": {
"$ref": "evaluation.json"
},
"Metric": {
"$ref": "metric.json"
},
"Dataset": {
"$ref": "dataset.json"
},
"Datasets": {
"$ref": "datasets.json"
} }
} }

View File

@@ -5,7 +5,7 @@
"nif:beginIndex": {"type": "integer"}, "nif:beginIndex": {"type": "integer"},
"nif:endIndex": {"type": "integer"}, "nif:endIndex": {"type": "integer"},
"nif:anchorOf": { "nif:anchorOf": {
"description": "Piece of context that contains the Emotion", "description": "Piece of context that contains the Sentiment",
"type": "string" "type": "string"
}, },
"onyx:hasDimension": { "onyx:hasDimension": {

View File

@@ -6,7 +6,7 @@
"type": "string" "type": "string"
}, },
"nif:isString": { "nif:isString": {
"description": "String contained in this Context. Alternative: nif:isString", "description": "String contained in this Context",
"type": "string" "type": "string"
}, },
"sentiments": { "sentiments": {

View File

@@ -1,28 +0,0 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"name": "Evaluation",
"properties": {
"@id": {
"type": "string"
},
"@type": {
"type": "array",
"default": "Evaluation"
},
"metrics": {
"type": "array",
"items": {"$ref": "metric.json" },
"default": []
},
"evaluatesOn": {
"description": "Name of the dataset evaluated ",
"type": "string"
},
"evaluates": {
"description": "Classifier or plugin evaluated",
"type": "string"
}
},
"required": ["@id", "metrics"]
}

View File

@@ -1,17 +0,0 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"allOf": [
{"$ref": "response.json"},
{
"title": "Help",
"description": "Help containing accepted parameters",
"type": "object",
"properties": {
"valid_parameters": {
"type": "object"
}
},
"required": "valid_parameters"
}
]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"properties": {
"@id": {
"type": "string"
},
"@type": {
"type": "string"
},
"maxValue": {
"type": "number"
},
"minValue": {
"type": "number"
},
"value": {
"type": "number"
},
"deviation": {
"type": "number"
}
},
"required": ["@id"]
}

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"type": "object", "type": "object",
"required": ["@id", "name", "description", "version", "plugin_type"], "required": ["@id", "extra_params"],
"properties": { "properties": {
"@id": { "@id": {
"type": "string", "type": "string",
@@ -9,19 +9,7 @@
}, },
"name": { "name": {
"type": "string", "type": "string",
"description": "The name of the plugin, which will be used in the algorithm detection phase." "description": "The name of the plugin, which will be used in the algorithm detection phase"
},
"description": {
"type": "string",
"description": "A summary of what the plugin does, and pointers to further information."
},
"version": {
"type": "string",
"description": "The version of the plugin."
},
"plugin_type": {
"type": "string",
"description": "Sub-type of plugin. e.g. sentimentPlugin"
}, },
"extra_params": { "extra_params": {
"type": "object", "type": "object",

View File

@@ -3,7 +3,6 @@
"allOf": [ "allOf": [
{"$ref": "response.json"}, {"$ref": "response.json"},
{ {
"required": ["plugins"],
"properties": { "properties": {
"plugins": { "plugins": {
"type": "array", "type": "array",

View File

@@ -2,12 +2,7 @@
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"type": "object", "type": "object",
"properties": { "properties": {
"@type": {"type": "string"}, "@type": {"type": "string"}
"parameters": {
"type": "object",
"default": {}
}
}, },
"required": ["@type"] "required": ["@type"]

View File

@@ -152,18 +152,3 @@ textarea{
/* background: white; */ /* background: white; */
display: none; display: none;
} }
.deco-none {
color: inherit;
text-decoration: inherit;
}
.deco-none:link {
color: inherit;
text-decoration: inherit;
}
.deco-none:hover {
color: inherit;
text-decoration: inherit;
}

View File

@@ -1,9 +1,6 @@
var ONYX = "http://www.gsi.dit.upm.es/ontologies/onyx/ns#"; var ONYX = "http://www.gsi.dit.upm.es/ontologies/onyx/ns#";
var RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"; var RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
var plugins_params = default_params = {}; var plugins_params={};
var plugins = [];
var defaultPlugin = {};
var gplugins = {};
function replaceURLWithHTMLLinks(text) { function replaceURLWithHTMLLinks(text) {
console.log('Text: ' + text); console.log('Text: ' + text);
@@ -28,359 +25,112 @@ function hashchanged(){
} }
} }
function get_plugins(response){
plugins = response.plugins;
}
function get_datasets(response){
datasets = response.datasets
}
function group_plugins(){
for (r in plugins){
ptype = plugins[r]['@type'];
if(gplugins[ptype] == undefined){
gplugins[ptype] = [r];
}else{
gplugins[ptype].push(r);
}
}
}
function get_parameters(){
for (p in plugins){
plugin = plugins[p];
if (plugin["extra_params"]){
plugins_params[plugin["name"]] = plugin["extra_params"];
}
}
}
function draw_plugins_selection(){
html="";
group_plugins();
for (g in gplugins){
html += "<optgroup label=\""+g+"\">"
for (r in gplugins[g]){
plugin = plugins[gplugins[g][r]]
if (!plugin["name"]){
console.log("No name for plugin ", plugin);
continue;
}
html+= "<option value=\""+plugin.name+"\" "
if (plugin["name"] == defaultPlugin["name"]){
html+= " selected=\"selected\""
}
if (!plugin["is_activated"]){
html+= " disabled=\"disabled\" "
}
html+=">"+plugin["name"]+"</option>"
}
}
html += "</optgroup>"
// Two elements with plugin class
// One from the evaluate tab and another one from the analyse tab
document.getElementsByClassName('plugin')[0].innerHTML = html;
document.getElementsByClassName('plugin')[1].innerHTML = html;
}
function draw_plugins_list(){
var availablePlugins = document.getElementById('availablePlugins');
for(p in plugins){
var pluginEntry = document.createElement('li');
plugin = plugins[p];
newHtml = ""
if(plugin.url) {
newHtml= "<a href="+plugin.url+">" + plugin.name + "</a>";
}else {
newHtml= plugin["name"];
}
newHtml += ": " + replaceURLWithHTMLLinks(plugin.description);
pluginEntry.innerHTML = newHtml;
availablePlugins.appendChild(pluginEntry)
}
}
function draw_datasets(){
html = "";
repeated_html = "<input class=\"checks-datasets\" type=\"checkbox\" value=\"";
for (dataset in datasets){
html += repeated_html+datasets[dataset]["@id"]+"\">"+datasets[dataset]["@id"];
html += "<br>"
}
document.getElementById("datasets").innerHTML = html;
}
$(document).ready(function() { $(document).ready(function() {
var response = JSON.parse($.ajax({type: "GET", url: "/api/plugins/" , async: false}).responseText); var response = JSON.parse($.ajax({type: "GET", url: "/api/plugins/" , async: false}).responseText);
defaultPlugin= JSON.parse($.ajax({type: "GET", url: "/api/plugins/default" , async: false}).responseText); var defaultPlugin= JSON.parse($.ajax({type: "GET", url: "/api/plugins/default" , async: false}).responseText);
var response2 = JSON.parse($.ajax({type: "GET", url: "/api/datasets/" , async: false}).responseText); html="";
var availablePlugins = document.getElementById('availablePlugins');
plugins = response.plugins;
for (r in plugins){
plugin = plugins[r]
if (plugin["name"]){
if (plugin["name"] == defaultPlugin["name"]){
if (plugin["is_activated"]){
html+= "<option value=\""+plugin["name"]+"\" selected=\"selected\">"+plugin["name"]+"</option>"
}else{
html+= "<option value=\""+plugin["name"]+"\" selected=\"selected\" disabled=\"disabled\">"+plugin["name"]+"</option>"
}
}
else{
if (plugin["is_activated"]){
html+= "<option value=\""+plugin["name"]+"\">"+plugin["name"]+"</option>"
}
else{
html+= "<option value=\""+plugin["name"]+"\" disabled=\"disabled\">"+plugin["name"]+"</option>"
}
}
}
if (plugin["extra_params"]){
plugins_params[plugin["name"]]={};
for (param in plugin["extra_params"]){
if (typeof plugin["extra_params"][param] !="string"){
var params = new Array();
var alias = plugin["extra_params"][param]["aliases"][0];
params[alias]=new Array();
for (option in plugin["extra_params"][param]["options"]){
params[alias].push(plugin["extra_params"][param]["options"][option])
}
plugins_params[plugin["name"]][alias] = (params[alias])
}
}
}
var pluginList = document.createElement('li');
get_plugins(response); newHtml = ""
get_default_parameters(); if(plugin.url) {
get_datasets(response2); newHtml= "<a href="+plugin.url+">" + plugin.name + "</a>";
}else {
newHtml= plugin["name"];
}
newHtml += ": " + replaceURLWithHTMLLinks(plugin.description);
pluginList.innerHTML = newHtml;
availablePlugins.appendChild(pluginList)
}
document.getElementById('plugins').innerHTML = html;
change_params();
draw_plugins_list(); $(window).on('hashchange', hashchanged);
draw_plugins_selection(); hashchanged();
draw_parameters(); $('.tooltip-form').tooltip();
draw_datasets();
$(window).on('hashchange', hashchanged);
hashchanged();
$('.tooltip-form').tooltip();
}); });
function get_default_parameters(){
default_params = JSON.parse($.ajax({type: "GET", url: "/api?help=true" , async: false}).responseText).valid_parameters;
// Remove the parameters that are always added
delete default_params["input"];
delete default_params["algorithm"];
delete default_params["help"];
} function change_params(){
var plugin = document.getElementById("plugins").options[document.getElementById("plugins").selectedIndex].value;
function draw_default_parameters(){ html=""
var basic_params = document.getElementById("basic_params"); for (param in plugins_params[plugin]){
basic_params.innerHTML = params_div(default_params); if (param || plugins_params[plugin][param].length > 1){
} html+= "<label> Parameter "+param+"</label>"
html+= "<select id=\""+param+"\" name=\""+param+"\">"
function draw_extra_parameters(){ for (option in plugins_params[plugin][param]){
var plugin = document.getElementsByClassName('plugin')[0].options[document.getElementsByClassName('plugin')[0].selectedIndex].value; html+="<option value \""+plugins_params[plugin][param][option]+"\">"+plugins_params[plugin][param][option]+"</option>"
get_parameters();
var extra_params = document.getElementById("extra_params");
extra_params.innerHTML = params_div(plugins_params[plugin]);
}
function draw_parameters(){
draw_default_parameters();
draw_extra_parameters();
}
function add_default_params(){
var html = "";
// html += '<a href="#basic_params" class="btn btn-info" data-toggle="collapse">Basic API parameters</a>';
html += '<span id="basic_params" class="panel-collapse collapse">';
html += '<ul class="list-group">'
html += params_div(default_params);
html += '</span>';
return html;
}
function params_div(params){
var html = '<div class="container-fluid">';
if (Object.keys(params).length === 0) {
html += '<p class="text text-muted text-center">This plugin does not take any extra parameters</p>';
}
// Iterate over the keys in order
pnames = Object.keys(params).sort()
for (ix in pnames){
pname = pnames[ix];
param = params[pname];
html+='<div class="form-group">';
html += '<div class="row">'
html+= '<label class="col-sm-4" for="'+pname+'">'+pname+'</label>'
if (param.options){
opts = param.options;
if(param.options.length == 1 && param.options[0] == 'boolean') {
opts = [true, false];
} }
html+= '<select class="col-sm-8" id="'+pname+"\" name=\""+pname+"\">" }
var defaultopt = param.default; html+="</select>"
for (option in opts){
isselected = "";
if (defaultopt != undefined && opts[option] == defaultopt ){
isselected = ' selected="selected"'
}
html+="<option value=\""+opts[option]+'"' + isselected +
'>'+opts[option]+"</option>"
}
html+="</select>"
} }
else { document.getElementById("params").innerHTML = html
default_value = ""; };
if(param.default != undefined){
default_value = param.default;
};
html +='<input class="col-sm-8" id="'+pname+'" name="'+pname+'" value="' + default_value + '"></input>';
}
html+='</div>';
html+='<div class="row">';
if ('description' in param){
html += '<p class="form-text sm-sm-12 text-muted text-center">' + param.description + '</p>';
}
html+='</div>';
html+='</div>';
}
html+='</div>';
return html;
}
function _get_form_parameters(id){
var element = document.getElementById(id);
params = {};
var selects = element.getElementsByTagName('select');
var inputs = element.getElementsByTagName('input');
Array.prototype.forEach.call(selects, function (sel) {
key = sel.name;
value = sel.options[sel.selectedIndex].value
params[key] = value;
});
Array.prototype.forEach.call(inputs, function (el) {
params[el.name] = el.value;
});
for (k in params){
value = params[k];
if (value == "" || value === "undefined"){
delete params[k];
}
}
return params;
}
function get_form_parameters(){
var p1 = _get_form_parameters("basic_params");
var p2 = _get_form_parameters("extra_params");
return Object.assign(p1, p2);
}
function add_param(key, value){
value = encodeURIComponent(value);
return "&"+key+"="+value;
}
function load_JSON(){ function load_JSON(){
url = "/api"; url = "/api";
var container = document.getElementById('results'); var container = document.getElementById('results');
var rawcontainer = document.getElementById("jsonraw"); var rawcontainer = document.getElementById("jsonraw");
rawcontainer.innerHTML = ''; rawcontainer.innerHTML = '';
container.innerHTML = ''; container.innerHTML = '';
var plugin = document.getElementById("plugins").options[document.getElementById("plugins").selectedIndex].value;
var plugin = document.getElementsByClassName("plugin")[0].options[document.getElementsByClassName("plugin")[0].selectedIndex].value; var input = encodeURIComponent(document.getElementById("input").value);
url += "?algo="+plugin+"&i="+input
var input = encodeURIComponent(document.getElementById("input").value); for (param in plugins_params[plugin]){
url += "?algo="+plugin+"&i="+input if (param != null){
var param_value = encodeURIComponent(document.getElementById(param).options[document.getElementById(param).selectedIndex].text);
params = get_form_parameters(); if (param_value){
url+="&"+param+"="+param_value
for (key in params){ }
url += add_param(key, params[key]);
}
var response = $.ajax({type: "GET", url: url , async: false}).responseText;
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
document.getElementById("input_request").innerHTML = "<a href='"+url+"'>"+url+"</a>"
document.getElementById("results-div").style.display = 'block';
try {
response = JSON.parse(response);
var options = {
mode: 'view'
};
var editor = new JSONEditor(container, options, response);
editor.expandAll();
// $('#results-div a[href="#viewer"]').tab('show');
$('#results-div a[href="#viewer"]').click();
// location.hash = 'raw';
}
catch(err){
console.log("Error decoding JSON (got turtle?)");
$('#results-div a[href="#raw"]').click();
// location.hash = 'raw';
}
}
function get_datasets_from_checkbox(){
var checks = document.getElementsByClassName("checks-datasets");
datasets = "";
for (var i = 0; i < checks.length; i++){
if (checks[i].checked){
datasets += checks[i].value + ",";
} }
} }
datasets = datasets.slice(0, -1); var response = JSON.parse($.ajax({type: "GET", url: url , async: false}).responseText);
var options = {
mode: 'view'
};
var editor = new JSONEditor(container, options, response);
editor.expandAll();
rawcontainer.innerHTML = replaceURLWithHTMLLinks(JSON.stringify(response, undefined, 2))
document.getElementById("input_request").innerHTML = "<a href='"+url+"'>"+url+"</a>"
document.getElementById("results-div").style.display = 'block';
} }
function create_body_metrics(evaluations){
var new_tbody = document.createElement('tbody')
var metric_html = ""
for (var eval in evaluations){
metric_html += "<tr><th>"+evaluations[eval].evaluates+"</th><th>"+evaluations[eval].evaluatesOn+"</th>";
for (var metric in evaluations[eval].metrics){
metric_html += "<th>"+parseFloat(evaluations[eval].metrics[metric].value.toFixed(4))+"</th>";
}
metric_html += "</tr>";
}
new_tbody.innerHTML = metric_html
return new_tbody
}
function evaluate_JSON(){
url = "/api/evaluate";
var container = document.getElementById('results_eval');
var rawcontainer = document.getElementById('jsonraw_eval');
var table = document.getElementById("eval_table");
rawcontainer.innerHTML = "";
container.innerHTML = "";
var plugin = document.getElementsByClassName("plugin")[0].options[document.getElementsByClassName("plugin")[0].selectedIndex].value;
get_datasets_from_checkbox();
url += "?algo="+plugin+"&dataset="+datasets
var response = $.ajax({type: "GET", url: url , async: false, dataType: 'json'}).responseText;
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
document.getElementById("input_request_eval").innerHTML = "<a href='"+url+"'>"+url+"</a>"
document.getElementById("evaluate-div").style.display = 'block';
try {
response = JSON.parse(response);
var options = {
mode: 'view'
};
//Control the single response results
if (!(Array.isArray(response.evaluations))){
response.evaluations = [response.evaluations]
}
new_tbody = create_body_metrics(response.evaluations)
table.replaceChild(new_tbody, table.lastElementChild)
var editor = new JSONEditor(container, options, response);
editor.expandAll();
// $('#results-div a[href="#viewer"]').tab('show');
$('#evaluate-div a[href="#evaluate-table"]').click();
// location.hash = 'raw';
}
catch(err){
console.log("Error decoding JSON (got turtle?)");
$('#evaluate-div a[href="#evaluate-raw"]').click();
// location.hash = 'raw';
}
}

View File

@@ -32,8 +32,6 @@
<ul class="nav nav-tabs" role="tablist"> <ul class="nav nav-tabs" role="tablist">
<li role="presentation" ><a class="active" href="#about">About</a></li> <li role="presentation" ><a class="active" href="#about">About</a></li>
<li role="presentation"class="active"><a class="active" href="#test">Test it</a></li> <li role="presentation"class="active"><a class="active" href="#test">Test it</a></li>
<li role="presentation"><a class="active" href="#evaluate">Evaluate Plugins</a></li>
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
@@ -56,7 +54,6 @@
<ul> <ul>
<li>List all available plugins: <a href="/api/plugins">/api/plugins</a></li> <li>List all available plugins: <a href="/api/plugins">/api/plugins</a></li>
<li>Get information about the default plugin: <a href="/api/plugins/default">/api/plugins/default</a></li> <li>Get information about the default plugin: <a href="/api/plugins/default">/api/plugins/default</a></li>
<li>List all available datasets: <a href="/api/datasets">/api/datasets</a></li>
<li>Download the JSON-LD context used: <a href="/api/contexts/Results.jsonld">/api/contexts/Results.jsonld</a></li> <li>Download the JSON-LD context used: <a href="/api/contexts/Results.jsonld">/api/contexts/Results.jsonld</a></li>
</ul> </ul>
@@ -91,52 +88,28 @@
<div class="tab-pane active" id="test"> <div class="tab-pane active" id="test">
<div class="well"> <div class="well">
<form id="form" class="container" onsubmit="return getPlugins();" accept-charset="utf-8"> <form id="form" onsubmit="return getPlugins();" accept-charset="utf-8">
<div id="inputswrapper">
<div><textarea id="input" class="boxsizingBorder" rows="5" name="i">This text makes me sad. <div><textarea id="input" class="boxsizingBorder" rows="5" name="i">This text makes me sad.
whilst this text makes me happy and surprised at the same time. whilst this text makes me happy and surprised at the same time.
I cannot believe it!</textarea> I cannot believe it!</textarea></div>
</div> <label>Select the plugin:</label>
<div> <select id="plugins" name="plugins" onchange="change_params()">
<label>Select the plugin:</label> </select>
<select id="plugins" name="plugins" class=plugin onchange="draw_extra_parameters()"> </br>
</select> <div id ="params">
</div>
<!-- PARAMETERS -->
<div class="panel-group" id="parameters">
<div class="panel panel-default">
<a data-toggle="collapse" class="deco-none" href="#basic_params">
<div class="panel-heading">
<h4 class="panel-title">
Basic API parameters
</h4>
</div>
</a>
<div id="basic_params" class="panel-collapse collapse panel-body">
</div> </div>
</div> </br>
<div class="panel panel-default">
<a data-toggle="collapse" class="deco-none" href="#extra_params">
<div class="panel-heading">
<h4 class="panel-title">
Plugin extra parameters
</h4>
</div>
</a>
<div id="extra_params" class="panel-collapse collapse in panel-body">
</div>
</div>
</div>
<!-- END PARAMETERS -->
<a id="preview" class="btn btn-lg btn-primary" onclick="load_JSON()">Analyse!</a> <a id="preview" class="btn btn-lg btn-primary" onclick="load_JSON()">Analyse!</a>
<!--<button id="visualise" name="type" type="button">Visualise!</button>--> <!--<button id="visualise" name="type" type="button">Visualise!</button>-->
</div>
</form> </form>
</div> </div>
<span id="input_request"></span> <span id="input_request"></span>
<div id="results-div"> <div id="results-div">
<ul class="nav nav-tabs" role="tablist"> <ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a data-toggle="tab" class="active" href="#viewer">Viewer</a></li> <li role="presentation" class="active"><a class="active" href="#viewer">Viewer</a></li>
<li role="presentation"><a data-toggle="tab" class="active" href="#raw">Raw</a></li> <li role="presentation"><a class="active" href="#raw">Raw</a></li>
</ul> </ul>
<div class="tab-content" id="results-container"> <div class="tab-content" id="results-container">
@@ -146,7 +119,7 @@ I cannot believe it!</textarea>
</div> </div>
</div> </div>
<div class="tab-pane" id="raw"> <div class="tab-pane" id="raw">
<div id="content"> <div id="content">
<pre id="jsonraw" class="results"></pre> <pre id="jsonraw" class="results"></pre>
</div> </div>
@@ -154,70 +127,6 @@ I cannot believe it!</textarea>
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane" id="evaluate">
<div class="well">
<form id="form" class="container" onsubmit="return getPlugins();" accept-charset="utf-8">
<div>
<label>Select the plugin:</label>
<select id="plugins-eval" name="plugins-eval" class=plugin onchange="draw_extra_parameters()">
</select>
</div>
<div>
<label>Select the datasets:</label>
<div id="datasets" name="datasets" >
</select>
</div>
<a id="preview" class="btn btn-lg btn-primary" onclick="evaluate_JSON()">Evaluate Plugin!</a>
<!--<button id="visualise" name="type" type="button">Visualise!</button>-->
</form>
</div>
<span id="input_request_eval"></span>
<div id="evaluate-div">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a data-toggle="tab" class="active" href="#evaluate-viewer">Viewer</a></li>
<li role="presentation"><a data-toggle="tab" class="active" href="#evaluate-raw">Raw</a></li>
<li role="presentation"><a data-toggle="tab" class="active" href="#evaluate-table">Table</a></li>
</ul>
<div class="tab-content" id="evaluate-container">
<div class="tab-pane active" id="evaluate-viewer">
<div id="content">
<pre id="results_eval" class="results_eval"></pre>
</div>
</div>
<div class="tab-pane" id="evaluate-raw">
<div id="content">
<pre id="jsonraw_eval" class="results_eval"></pre>
</div>
</div>
<div class="tab-pane" id="evaluate-table">
<table id="eval_table" class="table table-condensed">
<thead>
<tr>
<th>Plugin</th>
<th>Dataset</th>
<th>Accuracy</th>
<th>Precision_macro</th>
<th>Recall_macro</th>
<th>F1_macro</th>
<th>F1_weighted</th>
<th>F1_micro</th>
<th>F1</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<a href="http://www.gsi.dit.upm.es" target="_blank"><img class="center-block" src="static/img/gsi.png"/> </a> <a href="http://www.gsi.dit.upm.es" target="_blank"><img class="center-block" src="static/img/gsi.png"/> </a>
</div> </div>

View File

@@ -1,36 +0,0 @@
try:
from unittest.mock import patch, MagicMock
except ImportError:
from mock import patch, MagicMock
from past.builtins import basestring
import json
from contextlib import contextmanager
from .models import BaseModel
@contextmanager
def patch_requests(value, code=200):
success = MagicMock()
if isinstance(value, BaseModel):
value = value.jsonld()
if not isinstance(value, basestring):
data = json.dumps(value)
else:
data = value
success.json.return_value = value
success.status_code = code
success.content = data
success.text = data
method_mocker = MagicMock()
method_mocker.return_value = success
with patch.multiple('requests', request=method_mocker,
get=method_mocker, post=method_mocker):
yield method_mocker, success
assert method_mocker.called

View File

@@ -1,107 +0,0 @@
from . import models, __version__
from collections import MutableMapping
import pprint
import pdb
import logging
logger = logging.getLogger(__name__)
# MutableMapping should be enough, but it causes problems with py2
DICTCLASSES = (MutableMapping, dict, models.BaseModel)
def check_template(indict, template):
if isinstance(template, DICTCLASSES) and isinstance(indict, DICTCLASSES):
for k, v in template.items():
if k not in indict:
raise models.Error('{} not in {}'.format(k, indict))
check_template(indict[k], v)
elif isinstance(template, list) and isinstance(indict, list):
for e in template:
for i in indict:
try:
check_template(i, e)
break
except models.Error as ex:
# raise
continue
else:
raise models.Error(('Element not found.'
'\nExpected: {}\nIn: {}').format(pprint.pformat(e),
pprint.pformat(indict)))
else:
if indict != template:
raise models.Error(('Differences found.\n'
'\tExpected: {}\n'
'\tFound: {}').format(pprint.pformat(template),
pprint.pformat(indict)))
def convert_dictionary(original, mappings):
result = {}
for key, value in original.items():
if key in mappings:
key = mappings[key]
result[key] = value
return result
def easy_load(app=None, plugin_list=None, plugin_folder=None, **kwargs):
'''
Run a server with a specific plugin.
'''
from flask import Flask
from .extensions import Senpy
if not app:
app = Flask(__name__)
sp = Senpy(app, plugin_folder=plugin_folder, **kwargs)
if not plugin_list:
from . import plugins
import __main__
plugin_list = plugins.from_module(__main__)
for plugin in plugin_list:
sp.add_plugin(plugin)
sp.install_deps()
sp.activate_all()
return sp, app
def easy_test(plugin_list=None, debug=True):
logger.setLevel(logging.DEBUG)
logging.getLogger().setLevel(logging.INFO)
try:
if not plugin_list:
import __main__
logger.info('Loading classes from {}'.format(__main__))
from . import plugins
plugin_list = plugins.from_module(__main__)
for plug in plugin_list:
plug.test()
plug.log.info('My tests passed!')
logger.info('All tests passed!')
except Exception:
if not debug:
raise
pdb.post_mortem()
def easy(host='0.0.0.0', port=5000, debug=True, **kwargs):
'''
Run a server with a specific plugin.
'''
logging.getLogger().setLevel(logging.DEBUG)
logging.getLogger('senpy').setLevel(logging.INFO)
sp, app = easy_load(**kwargs)
easy_test(sp.plugins())
app.debug = debug
import time
logger.info(time.time())
logger.info('Senpy version {}'.format(__version__))
logger.info('Server running on port %s:%d. Ctrl+C to quit' % (host,
port))
app.debug = debug
app.run(host,
port,
debug=app.debug)

View File

@@ -1,20 +1,20 @@
import pip
from setuptools import setup from setuptools import setup
# parse_requirements() returns generator of pip.req.InstallRequirement objects
from pip.req import parse_requirements
from senpy import __version__
with open('senpy/VERSION') as f: try:
__version__ = f.read().strip() install_reqs = parse_requirements(
assert __version__ "requirements.txt", session=pip.download.PipSession())
test_reqs = parse_requirements(
"test-requirements.txt", session=pip.download.PipSession())
except AttributeError:
install_reqs = parse_requirements("requirements.txt")
test_reqs = parse_requirements("test-requirements.txt")
install_reqs = [str(ir.req) for ir in install_reqs]
def parse_requirements(filename): test_reqs = [str(ir.req) for ir in test_reqs]
""" load requirements from a pip requirements file """
with open(filename, 'r') as f:
lineiter = list(line.strip() for line in f)
return [line for line in lineiter if line and not line.startswith("#")]
install_reqs = parse_requirements("requirements.txt")
test_reqs = parse_requirements("test-requirements.txt")
extra_reqs = parse_requirements("extra-requirements.txt")
setup( setup(
@@ -34,9 +34,6 @@ setup(
install_requires=install_reqs, install_requires=install_reqs,
tests_require=test_reqs, tests_require=test_reqs,
setup_requires=['pytest-runner', ], setup_requires=['pytest-runner', ],
extras_require={
'evaluation': extra_reqs
},
include_package_data=True, include_package_data=True,
entry_points={ entry_points={
'console_scripts': 'console_scripts':

View File

@@ -1,5 +1,3 @@
mock mock
pytest-cov pytest-cov
pytest pytest
scikit-learn
numpy

View File

@@ -1,4 +1,4 @@
from senpy import AnalysisPlugin from senpy.plugins import AnalysisPlugin
import multiprocessing import multiprocessing
@@ -7,15 +7,10 @@ def _train(process_number):
return process_number return process_number
class Async(AnalysisPlugin): class AsyncPlugin(AnalysisPlugin):
'''An example of an asynchronous module'''
author = '@balkian'
version = '0.2'
async = True
def _do_async(self, num_processes): def _do_async(self, num_processes):
pool = multiprocessing.Pool(processes=num_processes) pool = multiprocessing.Pool(processes=num_processes)
values = sorted(pool.map(_train, range(num_processes))) values = pool.map(_train, range(num_processes))
return values return values
@@ -26,12 +21,3 @@ class Async(AnalysisPlugin):
values = self._do_async(2) values = self._do_async(2)
entry.async_values = values entry.async_values = values
yield entry yield entry
test_cases = [
{
'input': 'any',
'expected': {
'async_values': [0, 1]
}
}
]

View File

@@ -0,0 +1,8 @@
---
name: Async
module: asyncplugin
description: I am async
author: "@balkian"
version: '0.1'
async: true
extra_params: {}

View File

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

View File

@@ -0,0 +1,15 @@
{
"name": "Dummy",
"module": "dummy",
"description": "I am dummy",
"author": "@balkian",
"version": "0.1",
"extra_params": {
"example": {
"@id": "example_parameter",
"aliases": ["example", "ex"],
"required": false,
"default": 0
}
}
}

View File

@@ -0,0 +1,14 @@
{
"name": "DummyRequired",
"module": "dummy",
"description": "I am dummy",
"author": "@balkian",
"version": "0.1",
"extra_params": {
"example": {
"@id": "example_parameter",
"aliases": ["example", "ex"],
"required": true
}
}
}

View File

@@ -0,0 +1,11 @@
from senpy.plugins import AnalysisPlugin
from time import sleep
class SleepPlugin(AnalysisPlugin):
def activate(self, *args, **kwargs):
sleep(self.timeout)
def analyse_entry(self, entry, params):
sleep(float(params.get("timeout", self.timeout)))
yield entry

View File

@@ -0,0 +1,16 @@
{
"name": "Sleep",
"module": "sleep",
"description": "I am dummy",
"author": "@balkian",
"version": "0.1",
"timeout": 0.5,
"extra_params": {
"timeout": {
"@id": "timeout_sleep",
"aliases": ["timeout", "to"],
"required": false,
"default": 0
}
}
}

View File

@@ -11,63 +11,51 @@ class APITest(TestCase):
def test_api_params(self): def test_api_params(self):
"""The API should not define any required parameters without a default""" """The API should not define any required parameters without a default"""
parse_params({}, API_PARAMS) parse_params({}, spec=API_PARAMS)
def test_web_params(self): def test_web_params(self):
"""The WEB should not define any required parameters without a default""" """The WEB should not define any required parameters without a default"""
parse_params({}, WEB_PARAMS) parse_params({}, spec=WEB_PARAMS)
def test_basic(self): def test_basic(self):
a = {} a = {}
self.assertRaises(Error, parse_params, a) try:
self.assertRaises(Error, parse_params, a, NIF_PARAMS) parse_params(a, spec=NIF_PARAMS)
raise AssertionError()
except Error:
pass
a = {'input': 'hello'} a = {'input': 'hello'}
p = parse_params(a, NIF_PARAMS) p = parse_params(a, spec=NIF_PARAMS)
assert 'input' in p assert 'input' in p
b = {'i': 'hello'} b = {'i': 'hello'}
p = parse_params(b, NIF_PARAMS) p = parse_params(b, spec=NIF_PARAMS)
assert 'input' in p assert 'input' in p
def test_plugin(self): def test_plugin(self):
query = {} query = {}
plug_params = { plug_params = {
'hello': { 'hello': {
'aliases': ['hiya', 'hello'], 'aliases': ['hello', 'hiya'],
'required': True 'required': True
} }
} }
self.assertRaises(Error, parse_params, plug_params) try:
parse_params(query, spec=plug_params)
raise AssertionError()
except Error:
pass
query['hello'] = 'world' query['hello'] = 'world'
p = parse_params(query, plug_params) p = parse_params(query, spec=plug_params)
assert 'hello' in p assert 'hello' in p
assert p['hello'] == 'world' assert p['hello'] == 'world'
del query['hello'] del query['hello']
query['hiya'] = 'dlrow' query['hiya'] = 'dlrow'
p = parse_params(query, plug_params) p = parse_params(query, spec=plug_params)
assert 'hello' in p assert 'hello' in p
assert 'hiya' in p
assert p['hello'] == 'dlrow' assert p['hello'] == 'dlrow'
def test_parameters2(self):
in1 = {
'meaningcloud-key': 5
}
in2 = {
'apikey': 25
}
extra_params = {
"apikey": {
"aliases": [
"apikey",
"meaningcloud-key"
],
"required": True
}
}
p1 = parse_params(in1, extra_params)
p2 = parse_params(in2, extra_params)
assert (p2['apikey'] / p1['apikey']) == 5
def test_default(self): def test_default(self):
spec = { spec = {
'hello': { 'hello': {
@@ -75,17 +63,6 @@ class APITest(TestCase):
'default': 1 'default': 1
} }
} }
p = parse_params({}, spec) p = parse_params({}, spec=spec)
assert 'hello' in p assert 'hello' in p
assert p['hello'] == 1 assert p['hello'] == 1
def test_call(self):
call = {
'input': "Aloha my friend",
'algo': "Dummy"
}
p = parse_params(call, API_PARAMS, NIF_PARAMS)
assert 'algorithm' in p
assert "Dummy" in p['algorithm']
assert 'input' in p
assert p['input'] == 'Aloha my friend'

View File

@@ -17,27 +17,21 @@ def parse_resp(resp):
class BlueprintsTest(TestCase): class BlueprintsTest(TestCase):
@classmethod def setUp(self):
def setUpClass(cls): self.app = Flask("test_extensions")
"""Set up only once, and re-use in every individual test""" self.app.debug = False
cls.app = Flask("test_extensions") self.client = self.app.test_client()
cls.app.debug = False self.senpy = Senpy()
cls.client = cls.app.test_client() self.senpy.init_app(self.app)
cls.senpy = Senpy(default_plugins=True) self.dir = os.path.join(os.path.dirname(__file__), "..")
cls.senpy.init_app(cls.app) self.senpy.add_folder(self.dir)
cls.dir = os.path.join(os.path.dirname(__file__), "..") self.senpy.activate_plugin("Dummy", sync=True)
cls.senpy.add_folder(cls.dir) self.senpy.activate_plugin("DummyRequired", sync=True)
cls.senpy.activate_plugin("Dummy", sync=True) self.senpy.default_plugin = 'Dummy'
cls.senpy.activate_plugin("DummyRequired", sync=True)
cls.senpy.default_plugin = 'Dummy'
def assertCode(self, resp, code): def assertCode(self, resp, code):
self.assertEqual(resp.status_code, code) self.assertEqual(resp.status_code, code)
def test_playground(self):
resp = self.client.get("/")
assert "main.js" in resp.data.decode()
def test_home(self): def test_home(self):
""" """
Calling with no arguments should ask the user for more arguments Calling with no arguments should ask the user for more arguments
@@ -58,7 +52,7 @@ class BlueprintsTest(TestCase):
The dummy plugin returns an empty response,\ The dummy plugin returns an empty response,\
it should contain the context it should contain the context
""" """
resp = self.client.get("/api/?i=My aloha mohame&with_parameters=True") resp = self.client.get("/api/?i=My aloha mohame")
self.assertCode(resp, 200) self.assertCode(resp, 200)
js = parse_resp(resp) js = parse_resp(resp)
logging.debug("Got response: %s", js) logging.debug("Got response: %s", js)
@@ -69,7 +63,7 @@ class BlueprintsTest(TestCase):
""" """
Extra params that have a default should Extra params that have a default should
""" """
resp = self.client.get("/api/?i=My aloha mohame&algo=Dummy&with_parameters=true") resp = self.client.get("/api/?i=My aloha mohame&algo=Dummy")
self.assertCode(resp, 200) self.assertCode(resp, 200)
js = parse_resp(resp) js = parse_resp(resp)
logging.debug("Got response: %s", js) logging.debug("Got response: %s", js)
@@ -81,23 +75,17 @@ class BlueprintsTest(TestCase):
Extra params that have a required argument that does not Extra params that have a required argument that does not
have a default should raise an error. have a default should raise an error.
""" """
self.app.debug = False
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired") resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired")
self.assertCode(resp, 400) self.assertCode(resp, 400)
js = parse_resp(resp) js = parse_resp(resp)
logging.debug("Got response: %s", js) logging.debug("Got response: %s", js)
assert isinstance(js, models.Error) assert isinstance(js, models.Error)
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired&example=notvalid")
self.assertCode(resp, 400)
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired&example=a")
self.assertCode(resp, 200)
def test_error(self): def test_error(self):
""" """
The dummy plugin returns an empty response,\ The dummy plugin returns an empty response,\
it should contain the context it should contain the context
""" """
self.app.debug = False
resp = self.client.get("/api/?i=My aloha mohame&algo=DOESNOTEXIST") resp = self.client.get("/api/?i=My aloha mohame&algo=DOESNOTEXIST")
self.assertCode(resp, 404) self.assertCode(resp, 404)
js = parse_resp(resp) js = parse_resp(resp)
@@ -162,15 +150,5 @@ class BlueprintsTest(TestCase):
def test_schema(self): def test_schema(self):
resp = self.client.get("/api/schemas/definitions.json") resp = self.client.get("/api/schemas/definitions.json")
self.assertCode(resp, 200) self.assertCode(resp, 200)
assert "$schema" in resp.data.decode()
def test_help(self):
resp = self.client.get("/api/?help=true")
self.assertCode(resp, 200)
js = parse_resp(resp) js = parse_resp(resp)
assert "valid_parameters" in js assert "$schema" in js
assert "help" in js["valid_parameters"]
def test_conversion(self):
resp = self.client.get("/api/?input=hello&algo=emoRand&emotionModel=DOES NOT EXIST")
self.assertCode(resp, 404)

View File

@@ -1,6 +1,11 @@
import logging import logging
from functools import partial from functools import partial
try:
from unittest.mock import patch
except ImportError:
from mock import patch
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from unittest import TestCase from unittest import TestCase
@@ -12,8 +17,11 @@ class CLITest(TestCase):
def test_basic(self): def test_basic(self):
self.assertRaises(Error, partial(main_function, [])) self.assertRaises(Error, partial(main_function, []))
res = main_function(['--input', 'test', '--algo', 'rand', with patch('senpy.extensions.Senpy.analyse') as patched:
'--with-parameters', '--default-plugins']) main_function(['--input', 'test'])
assert res.parameters['input'] == 'test'
assert 'rand' in res.parameters['algorithm'] patched.assert_called_with(input='test')
assert res.parameters['input'] == 'test' with patch('senpy.extensions.Senpy.analyse') as patched:
main_function(['--input', 'test', '--algo', 'rand'])
patched.assert_called_with(input='test', algo='rand')

View File

@@ -1,9 +1,24 @@
from unittest import TestCase from unittest import TestCase
try:
from unittest.mock import patch
except ImportError:
from mock import patch
import json
from senpy.test import patch_requests
from senpy.client import Client from senpy.client import Client
from senpy.models import Results, Plugins, Error from senpy.models import Results, Plugins, Error
from senpy.plugins import AnalysisPlugin from senpy.plugins import AnalysisPlugin, default_plugin_type
class Call(dict):
def __init__(self, obj):
self.obj = obj.serialize()
self.status_code = 200
self.content = self.json()
def json(self):
return json.loads(self.obj)
class ModelsTest(TestCase): class ModelsTest(TestCase):
@@ -14,18 +29,20 @@ class ModelsTest(TestCase):
def test_client(self): def test_client(self):
endpoint = 'http://dummy/' endpoint = 'http://dummy/'
client = Client(endpoint) client = Client(endpoint)
with patch_requests(Results()) as (request, response): success = Call(Results())
with patch('requests.request', return_value=success) as patched:
resp = client.analyse('hello') resp = client.analyse('hello')
assert isinstance(resp, Results) assert isinstance(resp, Results)
request.assert_called_with( patched.assert_called_with(
url=endpoint + '/', method='GET', params={'input': 'hello'}) url=endpoint + '/', method='GET', params={'input': 'hello'})
with patch_requests(Error('Nothing')) as (request, response): error = Call(Error('Nothing'))
with patch('requests.request', return_value=error) as patched:
try: try:
client.analyse(input='hello', algorithm='NONEXISTENT') client.analyse(input='hello', algorithm='NONEXISTENT')
raise Exception('Exceptions should be raised. This is not golang') raise Exception('Exceptions should be raised. This is not golang')
except Error: except Error:
pass pass
request.assert_called_with( patched.assert_called_with(
url=endpoint + '/', url=endpoint + '/',
method='GET', method='GET',
params={'input': 'hello', params={'input': 'hello',
@@ -37,11 +54,12 @@ class ModelsTest(TestCase):
plugins = Plugins() plugins = Plugins()
p1 = AnalysisPlugin({'name': 'AnalysisP1', 'version': 0, 'description': 'No'}) p1 = AnalysisPlugin({'name': 'AnalysisP1', 'version': 0, 'description': 'No'})
plugins.plugins = [p1, ] plugins.plugins = [p1, ]
with patch_requests(plugins) as (request, response): success = Call(plugins)
with patch('requests.request', return_value=success) as patched:
response = client.plugins() response = client.plugins()
assert isinstance(response, dict) assert isinstance(response, dict)
assert len(response) == 1 assert len(response) == 1
assert "AnalysisP1" in response assert "AnalysisP1" in response
request.assert_called_with( patched.assert_called_with(
url=endpoint + '/plugins', method='GET', url=endpoint + '/plugins', method='GET',
params={}) params={'plugin_type': default_plugin_type})

View File

@@ -10,26 +10,18 @@ except ImportError:
from functools import partial from functools import partial
from senpy.extensions import Senpy from senpy.extensions import Senpy
from senpy import plugins
from senpy.models import Error, Results, Entry, EmotionSet, Emotion, Plugin from senpy.models import Error, Results, Entry, EmotionSet, Emotion, Plugin
from senpy import api
from flask import Flask from flask import Flask
from unittest import TestCase from unittest import TestCase
def analyse(instance, **kwargs):
request = api.parse_call(kwargs)
return instance.analyse(request)
class ExtensionsTest(TestCase): class ExtensionsTest(TestCase):
def setUp(self): def setUp(self):
self.app = Flask('test_extensions') self.app = Flask('test_extensions')
self.examples_dir = os.path.join(os.path.dirname(__file__), '..', 'example-plugins') self.dir = os.path.join(os.path.dirname(__file__))
self.senpy = Senpy(plugin_folder=self.examples_dir, self.senpy = Senpy(plugin_folder=self.dir,
app=self.app, app=self.app,
default_plugins=False) default_plugins=False)
self.senpy.deactivate_all()
self.senpy.activate_plugin("Dummy", sync=True) self.senpy.activate_plugin("Dummy", sync=True)
def test_init(self): def test_init(self):
@@ -42,47 +34,32 @@ class ExtensionsTest(TestCase):
def test_discovery(self): def test_discovery(self):
""" Discovery of plugins in given folders. """ """ Discovery of plugins in given folders. """
# noinspection PyProtectedMember # noinspection PyProtectedMember
print(self.senpy.plugins()) assert self.dir in self.senpy._search_folders
assert self.senpy.get_plugin("dummy") print(self.senpy.plugins)
assert "Dummy" in self.senpy.plugins
def test_add_delete(self): def test_enabling(self):
'''Should be able to add and delete new plugins. ''' """ Enabling a plugin """
new = plugins.Analysis(name='new', description='new', version=0)
self.senpy.add_plugin(new)
assert new in self.senpy.plugins()
self.senpy.delete_plugin(new)
assert new not in self.senpy.plugins()
def test_adding_folder(self):
""" It should be possible for senpy to look for plugins in more folders. """
senpy = Senpy(plugin_folder=None,
app=self.app,
default_plugins=False)
assert not senpy.analysis_plugins
senpy.add_folder(self.examples_dir)
assert senpy.analysis_plugins
self.assertRaises(AttributeError, senpy.add_folder, 'DOES NOT EXIST')
def test_installing(self):
""" Installing a plugin """
info = { info = {
'name': 'TestPip', 'name': 'TestPip',
'module': 'mynoop', 'module': 'dummy',
'description': None, 'description': None,
'requirements': ['noop'], 'requirements': ['noop'],
'version': 0 'version': 0
} }
module = plugins.from_info(info, root=self.examples_dir, install=True) root = os.path.join(self.dir, 'plugins', 'dummy_plugin')
assert module.name == 'TestPip' name, module = self.senpy._load_plugin_from_info(info, root=root)
assert name == 'TestPip'
assert module assert module
import noop import noop
dir(noop) dir(noop)
self.senpy.install_deps()
def test_enabling(self): def test_installing(self):
""" Enabling a plugin """ """ Enabling a plugin """
self.senpy.activate_all(sync=True) self.senpy.activate_all(sync=True)
assert len(self.senpy.plugins()) >= 3 assert len(self.senpy.plugins) >= 3
assert self.senpy.get_plugin("Sleep").is_activated assert self.senpy.plugins["Sleep"].is_activated
def test_installing_nonexistent(self): def test_installing_nonexistent(self):
""" Fail if the dependencies cannot be met """ """ Fail if the dependencies cannot be met """
@@ -93,14 +70,15 @@ class ExtensionsTest(TestCase):
'requirements': ['IAmMakingThisPackageNameUpToFail'], 'requirements': ['IAmMakingThisPackageNameUpToFail'],
'version': 0 'version': 0
} }
root = os.path.join(self.dir, 'plugins', 'dummy_plugin')
with self.assertRaises(Error): with self.assertRaises(Error):
plugins.install_deps(info) name, module = self.senpy._load_plugin_from_info(info, root=root)
def test_disabling(self): def test_disabling(self):
""" Disabling a plugin """ """ Disabling a plugin """
self.senpy.deactivate_all(sync=True) self.senpy.deactivate_all(sync=True)
assert not self.senpy.get_plugin("dummy").is_activated assert not self.senpy.plugins["Dummy"].is_activated
assert not self.senpy.get_plugin("sleep").is_activated assert not self.senpy.plugins["Sleep"].is_activated
def test_default(self): def test_default(self):
""" Default plugin should be set """ """ Default plugin should be set """
@@ -113,28 +91,23 @@ class ExtensionsTest(TestCase):
def test_noplugin(self): def test_noplugin(self):
""" Don't analyse if there isn't any plugin installed """ """ Don't analyse if there isn't any plugin installed """
self.senpy.deactivate_all(sync=True) self.senpy.deactivate_all(sync=True)
self.assertRaises(Error, partial(analyse, self.senpy, input="tupni")) self.assertRaises(Error, partial(self.senpy.analyse, input="tupni"))
self.assertRaises(Error,
partial(
self.senpy.analyse,
input="tupni",
algorithm='Dummy'))
def test_analyse(self): def test_analyse(self):
""" Using a plugin """ """ Using a plugin """
# I was using mock until plugin started inheriting # I was using mock until plugin started inheriting
# Leaf (defaultdict with __setattr__ and __getattr__. # Leaf (defaultdict with __setattr__ and __getattr__.
r1 = analyse(self.senpy, algorithm="Dummy", input="tupni", output="tuptuo") r1 = self.senpy.analyse(
r2 = analyse(self.senpy, input="tupni", output="tuptuo") algorithm="Dummy", input="tupni", output="tuptuo")
r2 = self.senpy.analyse(input="tupni", output="tuptuo")
assert r1.analysis[0] == "plugins/Dummy_0.1" assert r1.analysis[0] == "plugins/Dummy_0.1"
assert r2.analysis[0] == "plugins/Dummy_0.1" assert r2.analysis[0] == "plugins/Dummy_0.1"
assert r1.entries[0]['nif:isString'] == 'input' assert r1.entries[0].text == 'input'
def test_analyse_empty(self):
""" Trying to analyse when no plugins are installed should raise an error."""
senpy = Senpy(plugin_folder=None,
app=self.app,
default_plugins=False)
self.assertRaises(Error, senpy.analyse, Results())
def test_analyse_wrong(self):
""" Trying to analyse with a non-existent plugin should raise an error."""
self.assertRaises(Error, analyse, self.senpy, algorithm='DOES NOT EXIST', input='test')
def test_analyse_jsonld(self): def test_analyse_jsonld(self):
""" Using a plugin with JSON-LD input""" """ Using a plugin with JSON-LD input"""
@@ -143,59 +116,55 @@ class ExtensionsTest(TestCase):
"@type": "results", "@type": "results",
"entries": [ "entries": [
{"@id": "entry1", {"@id": "entry1",
"nif:isString": "tupni", "text": "tupni",
"@type": "entry" "@type": "entry"
} }
] ]
}''' }'''
r1 = analyse(self.senpy, r1 = self.senpy.analyse(algorithm="Dummy",
algorithm="Dummy", input=js_input,
input=js_input, informat="json-ld",
informat="json-ld", output="tuptuo")
output="tuptuo") r2 = self.senpy.analyse(input="tupni", output="tuptuo")
r2 = analyse(self.senpy,
input="tupni",
output="tuptuo")
assert r1.analysis[0] == "plugins/Dummy_0.1" assert r1.analysis[0] == "plugins/Dummy_0.1"
assert r2.analysis[0] == "plugins/Dummy_0.1" assert r2.analysis[0] == "plugins/Dummy_0.1"
assert r1.entries[0]['nif:isString'] == 'input' assert r1.entries[0].text == 'input'
def test_analyse_error(self): def test_analyse_error(self):
mm = mock.MagicMock() mm = mock.MagicMock()
mm.id = 'magic_mock' mm.id = 'magic_mock'
mm.name = 'mock' mm.analyse_entries.side_effect = Error('error on analysis', status=500)
mm.is_activated = True self.senpy.plugins['MOCK'] = mm
mm.analyse_entries.side_effect = Error('error in analysis', status=500)
self.senpy.add_plugin(mm)
try: try:
analyse(self.senpy, input='nothing', algorithm='MOCK') self.senpy.analyse(input='nothing', algorithm='MOCK')
assert False assert False
except Error as ex: except Error as ex:
assert 'error in analysis' in ex['message'] assert ex['message'] == 'error on analysis'
assert ex['status'] == 500 assert ex['status'] == 500
ex = Exception('generic exception on analysis') mm.analyse.side_effect = Exception('generic exception on analysis')
mm.analyse.side_effect = ex mm.analyse_entries.side_effect = Exception(
mm.analyse_entries.side_effect = ex 'generic exception on analysis')
try: try:
analyse(self.senpy, input='nothing', algorithm='MOCK') self.senpy.analyse(input='nothing', algorithm='MOCK')
assert False assert False
except Exception as ex: except Error as ex:
assert 'generic exception on analysis' in str(ex) assert ex['message'] == 'generic exception on analysis'
assert ex['status'] == 500
def test_filtering(self): def test_filtering(self):
""" Filtering plugins """ """ Filtering plugins """
assert len(self.senpy.plugins(name="Dummy")) > 0 assert len(self.senpy.filter_plugins(name="Dummy")) > 0
assert not len(self.senpy.plugins(name="NotDummy")) assert not len(self.senpy.filter_plugins(name="notdummy"))
assert self.senpy.plugins(name="Dummy", is_activated=True) assert self.senpy.filter_plugins(name="Dummy", is_activated=True)
self.senpy.deactivate_plugin("Dummy", sync=True) self.senpy.deactivate_plugin("Dummy", sync=True)
assert not len(self.senpy.plugins(name="Dummy", assert not len(
is_activated=True)) self.senpy.filter_plugins(name="Dummy", is_activated=True))
def test_load_default_plugins(self): def test_load_default_plugins(self):
senpy = Senpy(plugin_folder=self.examples_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): def test_convert_emotions(self):
self.senpy.activate_all(sync=True) self.senpy.activate_all(sync=True)
@@ -204,34 +173,47 @@ class ExtensionsTest(TestCase):
'onyx:usesEmotionModel': 'emoml:fsre-dimensions' 'onyx:usesEmotionModel': 'emoml:fsre-dimensions'
}) })
eSet1 = EmotionSet() eSet1 = EmotionSet()
eSet1.prov__wasGeneratedBy = plugin['@id'] eSet1.prov__wasGeneratedBy = plugin['id']
eSet1['onyx:hasEmotion'].append(Emotion({ eSet1['onyx:hasEmotion'].append(Emotion({
'emoml:arousal': 1, 'emoml:arousal': 1,
'emoml:potency': 0, 'emoml:potency': 0,
'emoml:valence': 0 'emoml:valence': 0
})) }))
response = Results({ response = Results({
'analysis': [{'plugin': plugin}],
'entries': [Entry({ 'entries': [Entry({
'nif:isString': 'much ado about nothing', 'text': 'much ado about nothing',
'emotions': [eSet1] 'emotions': [eSet1]
})] })]
}) })
params = {'emotionModel': 'emoml:big6', params = {'emotionModel': 'emoml:big6',
'conversion': 'full'} 'conversion': 'full'}
r1 = deepcopy(response) r1 = deepcopy(response)
r1.parameters = params self.senpy.convert_emotions(r1,
self.senpy.convert_emotions(r1) [plugin, ],
params)
assert len(r1.entries[0].emotions) == 2 assert len(r1.entries[0].emotions) == 2
params['conversion'] = 'nested' params['conversion'] = 'nested'
r2 = deepcopy(response) r2 = deepcopy(response)
r2.parameters = params self.senpy.convert_emotions(r2,
self.senpy.convert_emotions(r2) [plugin, ],
params)
assert len(r2.entries[0].emotions) == 1 assert len(r2.entries[0].emotions) == 1
assert r2.entries[0].emotions[0]['prov:wasDerivedFrom'] == eSet1 assert r2.entries[0].emotions[0]['prov:wasDerivedFrom'] == eSet1
params['conversion'] = 'filtered' params['conversion'] = 'filtered'
r3 = deepcopy(response) r3 = deepcopy(response)
r3.parameters = params self.senpy.convert_emotions(r3,
self.senpy.convert_emotions(r3) [plugin, ],
params)
assert len(r3.entries[0].emotions) == 1 assert len(r3.entries[0].emotions) == 1
r3.jsonld() r3.jsonld()
# def test_async_plugin(self):
# """ We should accept multiprocessing plugins with async=False"""
# thread1 = self.senpy.activate_plugin("Async", sync=False)
# thread1.join(timeout=1)
# assert len(self.senpy.plugins['Async'].value) == 4
# resp = self.senpy.analyse(input='nothing', algorithm='Async')
# assert len(resp.entries[0].async_values) == 2
# self.senpy.activate_plugin("Async", sync=True)

View File

@@ -9,15 +9,13 @@ from senpy.models import (Emotion,
EmotionAnalysis, EmotionAnalysis,
EmotionSet, EmotionSet,
Entry, Entry,
Entity,
Error, Error,
Results, Results,
Sentiment, Sentiment,
SentimentPlugin,
Plugins, Plugins,
Plugin,
from_string, from_string,
from_dict, from_dict)
subtypes)
from senpy import plugins from senpy import plugins
from pprint import pprint from pprint import pprint
@@ -101,19 +99,18 @@ class ModelsTest(TestCase):
def test_plugins(self): def test_plugins(self):
self.assertRaises(Error, plugins.Plugin) self.assertRaises(Error, plugins.Plugin)
p = plugins.SentimentPlugin({"name": "dummy", p = plugins.Plugin({"name": "dummy",
"description": "I do nothing", "version": 0,
"version": 0, "extra_params": {
"extra_params": { "none": {
"none": { "options": ["es", ],
"options": ["es", ], "required": False,
"required": False, "default": "0"
"default": "0" }
} }})
}})
c = p.jsonld() c = p.jsonld()
assert '@type' in c assert '@type' in c
assert c['@type'] == 'sentimentPlugin' assert c['@type'] == 'plugin'
assert 'info' not in c assert 'info' not in c
assert 'repo' not in c assert 'repo' not in c
assert 'extra_params' in c assert 'extra_params' in c
@@ -126,9 +123,7 @@ class ModelsTest(TestCase):
def test_str(self): def test_str(self):
"""The string representation shouldn't include private variables""" """The string representation shouldn't include private variables"""
r = Results() r = Results()
p = plugins.Plugin({"name": "STR test", p = plugins.Plugin({"name": "STR test", "version": 0})
"description": "Test of private variables.",
"version": 0})
p._testing = 0 p._testing = 0
s = str(p) s = str(p)
assert "_testing" not in s assert "_testing" not in s
@@ -136,11 +131,6 @@ class ModelsTest(TestCase):
s = str(r) s = str(r)
assert "_testing" not in s assert "_testing" not in s
def test_serialize(self):
for k, v in subtypes().items():
e = v()
e.serialize()
def test_turtle(self): def test_turtle(self):
"""Any model should be serializable as a turtle file""" """Any model should be serializable as a turtle file"""
ana = EmotionAnalysis() ana = EmotionAnalysis()
@@ -180,14 +170,13 @@ class ModelsTest(TestCase):
def test_single_plugin(self): def test_single_plugin(self):
"""A response with a single plugin should still return a list""" """A response with a single plugin should still return a list"""
plugs = Plugins() plugs = Plugins()
p = SentimentPlugin({'id': str(1), p = Plugin({'id': str(1),
'version': 0, 'version': 0,
'description': 'dummy'}) 'description': 'dummy'})
plugs.plugins.append(p) plugs.plugins.append(p)
assert isinstance(plugs.plugins, list) assert isinstance(plugs.plugins, list)
js = plugs.jsonld() js = plugs.jsonld()
assert isinstance(js['plugins'], list) assert isinstance(js['plugins'], list)
assert js['plugins'][0]['@type'] == 'sentimentPlugin'
def test_from_string(self): def test_from_string(self):
results = { results = {
@@ -196,11 +185,10 @@ class ModelsTest(TestCase):
'entries': [{ 'entries': [{
'@id': 'entry1', '@id': 'entry1',
'@type': 'entry', '@type': 'entry',
'nif:isString': 'TEST' 'text': 'TEST'
}] }]
} }
recovered = from_dict(results) recovered = from_dict(results)
assert recovered.id == results['@id']
assert isinstance(recovered, Results) assert isinstance(recovered, Results)
assert isinstance(recovered.entries[0], Entry) assert isinstance(recovered.entries[0], Entry)
@@ -208,14 +196,3 @@ class ModelsTest(TestCase):
recovered = from_string(string) recovered = from_string(string)
assert isinstance(recovered, Results) assert isinstance(recovered, Results)
assert isinstance(recovered.entries[0], Entry) assert isinstance(recovered.entries[0], Entry)
def test_serializable(self):
r = Results()
e = Entry()
ent = Entity()
e.entities.append(ent)
r.entries.append(e)
d = r.serializable()
assert d
assert d['entries']
assert d['entries'][0]['entities']

View File

@@ -1,25 +1,17 @@
#!/bin/env python #!/bin/env python
import os import os
import sys
import pickle import pickle
import shutil import shutil
import tempfile import tempfile
from unittest import TestCase, skipIf from unittest import TestCase
from senpy.models import Results, Entry, EmotionSet, Emotion, Plugins from senpy.models import Results, Entry, EmotionSet, Emotion
from senpy import plugins from senpy.plugins import SentimentPlugin, ShelfMixin
from senpy.plugins.conversion.emotion.centroids import CentroidConversion from senpy.plugins.conversion.emotion.centroids import CentroidConversion
import pandas as pd
class ShelfDummyPlugin(plugins.SentimentPlugin, plugins.ShelfMixin):
'''Dummy plugin for tests.'''
name = 'Shelf'
version = 0
author = 'the senpy community'
class ShelfDummyPlugin(SentimentPlugin, ShelfMixin):
def activate(self, *args, **kwargs): def activate(self, *args, **kwargs):
if 'counter' not in self.sh: if 'counter' not in self.sh:
self.sh['counter'] = 0 self.sh['counter'] = 0
@@ -41,6 +33,7 @@ class PluginsTest(TestCase):
def tearDown(self): def tearDown(self):
if os.path.exists(self.shelf_dir): if os.path.exists(self.shelf_dir):
shutil.rmtree(self.shelf_dir) shutil.rmtree(self.shelf_dir)
if os.path.isfile(self.shelf_file): if os.path.isfile(self.shelf_file):
os.remove(self.shelf_file) os.remove(self.shelf_file)
@@ -48,73 +41,38 @@ class PluginsTest(TestCase):
self.shelf_dir = tempfile.mkdtemp() self.shelf_dir = tempfile.mkdtemp()
self.shelf_file = os.path.join(self.shelf_dir, "shelf") self.shelf_file = os.path.join(self.shelf_dir, "shelf")
def test_serialize(self):
'''A plugin should be serializable and de-serializable'''
dummy = ShelfDummyPlugin()
dummy.serialize()
def test_jsonld(self):
'''A plugin should be serializable and de-serializable'''
dummy = ShelfDummyPlugin()
dummy.jsonld()
def test_shelf_file(self): def test_shelf_file(self):
a = ShelfDummyPlugin( a = ShelfDummyPlugin(
info={'name': 'default_shelve_file', info={'name': 'default_shelve_file',
'description': 'Dummy plugin for tests',
'version': 'test'}) 'version': 'test'})
a.activate() a.activate()
assert os.path.isfile(a.shelf_file) assert os.path.isfile(a.shelf_file)
os.remove(a.shelf_file) os.remove(a.shelf_file)
def test_plugin_filter(self):
ps = Plugins()
for i in (plugins.SentimentPlugin,
plugins.EmotionPlugin,
plugins.AnalysisPlugin):
p = i(name='Plugin_{}'.format(i.__name__),
description='TEST',
version=0,
author='NOBODY')
ps.plugins.append(p)
assert len(ps.plugins) == 3
cases = [('AnalysisPlugin', 3),
('SentimentPlugin', 1),
('EmotionPlugin', 1)]
for name, num in cases:
res = list(plugins.pfilter(ps.plugins, plugin_type=name))
assert len(res) == num
def test_shelf(self): def test_shelf(self):
''' A shelf is created and the value is stored ''' ''' A shelf is created and the value is stored '''
newfile = self.shelf_file + "new"
a = ShelfDummyPlugin(info={ a = ShelfDummyPlugin(info={
'name': 'shelve', 'name': 'shelve',
'description': 'Shelf plugin for tests',
'version': 'test', 'version': 'test',
'shelf_file': newfile 'shelf_file': self.shelf_file
}) })
assert a.sh == {} assert a.sh == {}
a.activate() a.activate()
assert a.sh == {'counter': 0} assert a.sh == {'counter': 0}
assert a.shelf_file == newfile assert a.shelf_file == self.shelf_file
a.sh['a'] = 'fromA' a.sh['a'] = 'fromA'
assert a.sh['a'] == 'fromA' assert a.sh['a'] == 'fromA'
a.save() a.save()
sh = pickle.load(open(newfile, 'rb')) sh = pickle.load(open(self.shelf_file, 'rb'))
assert sh['a'] == 'fromA' assert sh['a'] == 'fromA'
def test_dummy_shelf(self): def test_dummy_shelf(self):
with open(self.shelf_file, 'wb') as f:
pickle.dump({'counter': 99}, f)
a = ShelfDummyPlugin(info={ a = ShelfDummyPlugin(info={
'name': 'DummyShelf', 'name': 'DummyShelf',
'description': 'Dummy plugin for tests',
'shelf_file': self.shelf_file, 'shelf_file': self.shelf_file,
'version': 'test' 'version': 'test'
}) })
@@ -122,13 +80,9 @@ class PluginsTest(TestCase):
assert a.shelf_file == self.shelf_file assert a.shelf_file == self.shelf_file
res1 = a.analyse(input=1) res1 = a.analyse(input=1)
assert res1.entries[0].nif__isString == 100 assert res1.entries[0].nif__isString == 1
a.deactivate() res2 = a.analyse(input=1)
del a assert res2.entries[0].nif__isString == 2
with open(self.shelf_file, 'rb') as f:
sh = pickle.load(f)
assert sh['counter'] == 100
def test_corrupt_shelf(self): def test_corrupt_shelf(self):
''' Reusing the values of a previous shelf ''' ''' Reusing the values of a previous shelf '''
@@ -145,8 +99,7 @@ class PluginsTest(TestCase):
with open(fn, 'rb') as f: with open(fn, 'rb') as f:
msg, error = files[fn] msg, error = files[fn]
a = ShelfDummyPlugin(info={ a = ShelfDummyPlugin(info={
'name': 'test_corrupt_shelf_{}'.format(msg), 'name': 'shelve',
'description': 'Dummy plugin for tests',
'version': 'test', 'version': 'test',
'shelf_file': f.name 'shelf_file': f.name
}) })
@@ -167,7 +120,6 @@ class PluginsTest(TestCase):
''' Reusing the values of a previous shelf ''' ''' Reusing the values of a previous shelf '''
a = ShelfDummyPlugin(info={ a = ShelfDummyPlugin(info={
'name': 'shelve', 'name': 'shelve',
'description': 'Dummy plugin for tests',
'version': 'test', 'version': 'test',
'shelf_file': self.shelf_file 'shelf_file': self.shelf_file
}) })
@@ -178,7 +130,6 @@ class PluginsTest(TestCase):
b = ShelfDummyPlugin(info={ b = ShelfDummyPlugin(info={
'name': 'shelve', 'name': 'shelve',
'description': 'Dummy plugin for tests',
'version': 'test', 'version': 'test',
'shelf_file': self.shelf_file 'shelf_file': self.shelf_file
}) })
@@ -191,7 +142,6 @@ class PluginsTest(TestCase):
''' Should be able to set extra parameters''' ''' Should be able to set extra parameters'''
a = ShelfDummyPlugin(info={ a = ShelfDummyPlugin(info={
'name': 'shelve', 'name': 'shelve',
'description': 'Dummy shelf plugin for tests',
'version': 'test', 'version': 'test',
'shelf_file': self.shelf_file, 'shelf_file': self.shelf_file,
'extra_params': { 'extra_params': {
@@ -204,61 +154,6 @@ class PluginsTest(TestCase):
}) })
assert 'example' in a.extra_params assert 'example' in a.extra_params
def test_box(self):
class MyBox(plugins.Box):
''' Vague description'''
author = 'me'
version = 0
def input(self, entry, **kwargs):
return entry.text
def predict_one(self, input):
return 'SIGN' in input
def output(self, output, entry, **kwargs):
if output:
entry.myAnnotation = 'DETECTED'
return entry
test_cases = [
{
'input': "nothing here",
'expected': {'myAnnotation': 'DETECTED'},
'should_fail': True
}, {
'input': "SIGN",
'expected': {'myAnnotation': 'DETECTED'}
}]
MyBox().test()
def test_sentimentbox(self):
class SentimentBox(plugins.MappingMixin, plugins.SentimentBox):
''' Vague description'''
author = 'me'
version = 0
mappings = {'happy': 'marl:Positive', 'sad': 'marl:Negative'}
def predict_one(self, input, **kwargs):
return 'happy' if ':)' in input else 'sad'
test_cases = [
{
'input': 'a happy face :)',
'polarity': 'marl:Positive'
}, {
'input': "Nothing",
'polarity': 'marl:Negative'
}]
SentimentBox().test()
def test_conversion_centroids(self): def test_conversion_centroids(self):
info = { info = {
"name": "CentroidTest", "name": "CentroidTest",
@@ -280,7 +175,6 @@ class PluginsTest(TestCase):
"centroids_direction": ["emoml:big6", "emoml:fsre-dimensions"] "centroids_direction": ["emoml:big6", "emoml:fsre-dimensions"]
} }
c = CentroidConversion(info) c = CentroidConversion(info)
print(c.serialize())
es1 = EmotionSet() es1 = EmotionSet()
e1 = Emotion() e1 = Emotion()
@@ -289,7 +183,6 @@ class PluginsTest(TestCase):
res = c._forward_conversion(es1) res = c._forward_conversion(es1)
assert res["X-dimension"] == 0.5 assert res["X-dimension"] == 0.5
assert res["Y-dimension"] == 0.5 assert res["Y-dimension"] == 0.5
print(res)
e2 = Emotion() e2 = Emotion()
e2.onyx__hasEmotionCategory = "c2" e2.onyx__hasEmotionCategory = "c2"
@@ -297,74 +190,15 @@ class PluginsTest(TestCase):
res = c._forward_conversion(es1) res = c._forward_conversion(es1)
assert res["X-dimension"] == 0 assert res["X-dimension"] == 0
assert res["Y-dimension"] == 1 assert res["Y-dimension"] == 1
print(res)
e = Emotion() e = Emotion()
e["X-dimension"] = -0.2 e["X-dimension"] = -0.2
e["Y-dimension"] = -0.3 e["Y-dimension"] = -0.3
res = c._backwards_conversion(e) res = c._backwards_conversion(e)
assert res["onyx:hasEmotionCategory"] == "c3" assert res["onyx:hasEmotionCategory"] == "c3"
print(res)
e = Emotion() e = Emotion()
e["X-dimension"] = -0.2 e["X-dimension"] = -0.2
e["Y-dimension"] = 0.3 e["Y-dimension"] = 0.3
res = c._backwards_conversion(e) res = c._backwards_conversion(e)
assert res["onyx:hasEmotionCategory"] == "c2" assert res["onyx:hasEmotionCategory"] == "c2"
@skipIf(sys.version_info < (3, 0),
reason="requires Python3")
def test_evaluation(self):
testdata = []
for i in range(50):
testdata.append(["good", 1])
for i in range(50):
testdata.append(["bad", 0])
dataset = pd.DataFrame(testdata, columns=['text', 'polarity'])
class DummyPlugin(plugins.TextBox):
description = 'Plugin to test evaluation'
version = 0
def predict_one(self, input):
return 0
class SmartPlugin(plugins.TextBox):
description = 'Plugin to test evaluation'
version = 0
def predict_one(self, input):
if input == 'good':
return 1
return 0
dpipe = DummyPlugin()
results = plugins.evaluate(datasets={'testdata': dataset}, plugins=[dpipe], flatten=True)
dumb_metrics = results[0].metrics[0]
assert abs(dumb_metrics['accuracy'] - 0.5) < 0.01
spipe = SmartPlugin()
results = plugins.evaluate(datasets={'testdata': dataset}, plugins=[spipe], flatten=True)
smart_metrics = results[0].metrics[0]
assert abs(smart_metrics['accuracy'] - 1) < 0.01
def make_mini_test(fpath):
def mini_test(self):
for plugin in plugins.from_path(fpath, install=True):
plugin.test()
return mini_test
def _add_tests():
root = os.path.join(os.path.dirname(__file__), '..')
print(root)
for fpath in plugins.find_plugins([root, ]):
pass
t_method = make_mini_test(fpath)
t_method.__name__ = 'test_plugin_{}'.format(fpath)
setattr(PluginsTest, t_method.__name__, t_method)
del t_method
_add_tests()

Some files were not shown because too many files have changed in this diff Show More