mirror of
https://github.com/gsi-upm/senpy
synced 2025-10-19 09:48:26 +00:00
Compare commits
58 Commits
0.9.6
...
51-calcula
Author | SHA1 | Date | |
---|---|---|---|
|
d145a852e7 | ||
|
c090501534 | ||
|
6a1069780b | ||
|
41aa142ce0 | ||
|
b48730137d | ||
|
f1ec057b16 | ||
|
f6ca82cac8 | ||
|
318acd5a71 | ||
|
c8f6f5613d | ||
|
748d1a00bd | ||
|
a82e4ed440 | ||
|
c939b095de | ||
|
ca69bddc17 | ||
|
aa35e62a27 | ||
|
6dd4a44924 | ||
|
4291c5eabf | ||
|
7c7a815d1a | ||
|
a3eb8f196c | ||
|
00ffbb3804 | ||
|
13cf0c71c5 | ||
|
e5662d482e | ||
|
61181db199 | ||
|
a1663a3f31 | ||
|
83b23dbdf4 | ||
|
4675d9acf1 | ||
|
6832a2816d | ||
|
7a8abf1823 | ||
|
a21ce0d90e | ||
|
a964e586d7 | ||
|
bce42b5bb4 | ||
|
1313853788 | ||
|
697e779767 | ||
|
48f5ffafa1 | ||
|
73f7cbbe8a | ||
|
07a41236f8 | ||
|
55db97cf62 | ||
|
d8dead1908 | ||
|
87dcdb9fbc | ||
|
67ef4b60bd | ||
|
da4b11e5b5 | ||
|
c0aa7ddc3c | ||
|
5e2ada1654 | ||
|
7a188586c5 | ||
|
b768b215c5 | ||
|
d1f1b9a15a | ||
|
52a0f3f4c8 | ||
|
55c32dcd7c | ||
|
0093bc34d5 | ||
|
67bae9a20d | ||
|
551a5cb176 | ||
|
d6f4cc2dd2 | ||
|
4af692091a | ||
|
ec68ff0b90 | ||
|
738da490db | ||
|
d29c42fd2e | ||
|
23c88d0acc | ||
|
dcaaa591b7 | ||
|
15ab5f4c25 |
@@ -18,6 +18,8 @@ before_script:
|
|||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
- make -e test-$PYTHON_VERSION
|
- make -e test-$PYTHON_VERSION
|
||||||
|
except:
|
||||||
|
- tags # Avoid unnecessary double testing
|
||||||
|
|
||||||
test-3.5:
|
test-3.5:
|
||||||
<<: *test_definition
|
<<: *test_definition
|
||||||
@@ -29,29 +31,19 @@ test-2.7:
|
|||||||
variables:
|
variables:
|
||||||
PYTHON_VERSION: "2.7"
|
PYTHON_VERSION: "2.7"
|
||||||
|
|
||||||
.image: &image_definition
|
push:
|
||||||
stage: push
|
stage: push
|
||||||
script:
|
script:
|
||||||
- make -e push-$PYTHON_VERSION
|
- make -e push
|
||||||
only:
|
only:
|
||||||
- tags
|
- tags
|
||||||
- triggers
|
- triggers
|
||||||
- fix-makefiles
|
- fix-makefiles
|
||||||
|
|
||||||
push-3.5:
|
|
||||||
<<: *image_definition
|
|
||||||
variables:
|
|
||||||
PYTHON_VERSION: "3.5"
|
|
||||||
|
|
||||||
push-2.7:
|
|
||||||
<<: *image_definition
|
|
||||||
variables:
|
|
||||||
PYTHON_VERSION: "2.7"
|
|
||||||
|
|
||||||
push-latest:
|
push-latest:
|
||||||
<<: *image_definition
|
stage: push
|
||||||
variables:
|
script:
|
||||||
PYTHON_VERSION: latest
|
- make -e push-latest
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
- triggers
|
- triggers
|
||||||
|
@@ -1,5 +1,14 @@
|
|||||||
IMAGENAME?=$(NAME)
|
ifndef IMAGENAME
|
||||||
|
ifdef CI_REGISTRY_IMAGE
|
||||||
|
IMAGENAME=$(CI_REGISTRY_IMAGE)
|
||||||
|
else
|
||||||
|
IMAGENAME=$(NAME)
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
IMAGEWTAG?=$(IMAGENAME):$(VERSION)
|
IMAGEWTAG?=$(IMAGENAME):$(VERSION)
|
||||||
|
DOCKER_FLAGS?=$(-ti)
|
||||||
|
DOCKER_CMD?=
|
||||||
|
|
||||||
docker-login: ## Log in to the registry. It will only be used in the server, or when running a CI task locally (if CI_BUILD_TOKEN is set).
|
docker-login: ## Log in to the registry. It will only be used in the server, or when running a CI task locally (if CI_BUILD_TOKEN is set).
|
||||||
ifeq ($(CI_BUILD_TOKEN),)
|
ifeq ($(CI_BUILD_TOKEN),)
|
||||||
@@ -19,6 +28,19 @@ else
|
|||||||
@docker logout
|
@docker logout
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
docker-run: ## Build a generic docker image
|
||||||
|
docker run $(DOCKER_FLAGS) $(IMAGEWTAG) $(DOCKER_CMD)
|
||||||
|
|
||||||
|
docker-build: ## Build a generic docker image
|
||||||
|
docker build . -t $(IMAGEWTAG)
|
||||||
|
|
||||||
|
docker-push: docker-login ## Push a generic docker image
|
||||||
|
docker push $(IMAGEWTAG)
|
||||||
|
|
||||||
|
docker-latest-push: docker-login ## Push the latest image
|
||||||
|
docker tag $(IMAGEWTAG) $(IMAGENAME)
|
||||||
|
docker push $(IMAGENAME)
|
||||||
|
|
||||||
login:: docker-login
|
login:: docker-login
|
||||||
|
|
||||||
clean:: docker-clean
|
clean:: docker-clean
|
||||||
|
@@ -14,7 +14,7 @@ push-github: ## Push the code to github. You need to set up GITHUB_DEPLOY_KEY
|
|||||||
ifeq ($(GITHUB_DEPLOY_KEY),)
|
ifeq ($(GITHUB_DEPLOY_KEY),)
|
||||||
else
|
else
|
||||||
$(eval KEY_FILE := "$(shell mktemp)")
|
$(eval KEY_FILE := "$(shell mktemp)")
|
||||||
@echo "$(GITHUB_DEPLOY_KEY)" > $(KEY_FILE)
|
@printf '%b' '$(GITHUB_DEPLOY_KEY)' > $(KEY_FILE)
|
||||||
@git remote rm github-deploy || true
|
@git remote rm github-deploy || true
|
||||||
git remote add github-deploy $(GITHUB_REPO)
|
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 fetch github-deploy $(CI_COMMIT_REF_NAME)
|
||||||
@@ -22,7 +22,4 @@ else
|
|||||||
rm $(KEY_FILE)
|
rm $(KEY_FILE)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
push:: git-push
|
.PHONY:: commit tag git-push git-pull push-github
|
||||||
pull:: git-pull
|
|
||||||
|
|
||||||
.PHONY:: commit tag push git-push git-pull push-github
|
|
||||||
|
@@ -13,7 +13,7 @@
|
|||||||
KUBE_CA_TEMP=false
|
KUBE_CA_TEMP=false
|
||||||
ifndef KUBE_CA_PEM_FILE
|
ifndef KUBE_CA_PEM_FILE
|
||||||
KUBE_CA_PEM_FILE:=$$PWD/.ca.crt
|
KUBE_CA_PEM_FILE:=$$PWD/.ca.crt
|
||||||
CREATED:=$(shell echo -e "$(KUBE_CA_BUNDLE)" > $(KUBE_CA_PEM_FILE))
|
CREATED:=$(shell printf '%b\n' '$(KUBE_CA_BUNDLE)' > $(KUBE_CA_PEM_FILE))
|
||||||
endif
|
endif
|
||||||
KUBE_TOKEN?=""
|
KUBE_TOKEN?=""
|
||||||
KUBE_NAMESPACE?=$(NAME)
|
KUBE_NAMESPACE?=$(NAME)
|
||||||
|
@@ -1,17 +1,15 @@
|
|||||||
makefiles-remote:
|
makefiles-remote:
|
||||||
@git remote add makefiles ssh://git@lab.cluster.gsi.dit.upm.es:2200/docs/templates/makefiles.git 2>/dev/null || true
|
git ls-remote --exit-code makefiles 2> /dev/null || git remote add makefiles ssh://git@lab.cluster.gsi.dit.upm.es:2200/docs/templates/makefiles.git
|
||||||
|
|
||||||
makefiles-commit: makefiles-remote
|
makefiles-commit: makefiles-remote
|
||||||
git add -f .makefiles
|
git add -f .makefiles
|
||||||
git commit -em "Updated makefiles from ${NAME}"
|
git commit -em "Updated makefiles from ${NAME}"
|
||||||
|
|
||||||
makefiles-push:
|
makefiles-push:
|
||||||
|
git fetch makefiles $(NAME)
|
||||||
git subtree push --prefix=.makefiles/ makefiles $(NAME)
|
git subtree push --prefix=.makefiles/ makefiles $(NAME)
|
||||||
|
|
||||||
makefiles-pull: makefiles-remote
|
makefiles-pull: makefiles-remote
|
||||||
git subtree pull --prefix=.makefiles/ makefiles master --squash
|
git subtree pull --prefix=.makefiles/ makefiles master --squash
|
||||||
|
|
||||||
pull:: makefiles-pull
|
.PHONY:: makefiles-remote makefiles-commit makefiles-push makefiles-pull
|
||||||
push:: makefiles-push
|
|
||||||
|
|
||||||
.PHONY:: makefiles-remote makefiles-commit makefiles-push makefiles-pull pull push
|
|
||||||
|
@@ -26,6 +26,7 @@ Dockerfile-%: Dockerfile.template ## Generate a specific dockerfile (e.g. Docke
|
|||||||
quick_build: $(addprefix build-, $(PYMAIN))
|
quick_build: $(addprefix build-, $(PYMAIN))
|
||||||
|
|
||||||
build: $(addprefix build-, $(PYVERSIONS)) ## Build all images / python versions
|
build: $(addprefix build-, $(PYVERSIONS)) ## Build all images / python versions
|
||||||
|
docker tag $(IMAGEWTAG)-python$(PYMAIN) $(IMAGEWTAG)
|
||||||
|
|
||||||
build-%: version Dockerfile-% ## Build a specific version (e.g. build-2.7)
|
build-%: version Dockerfile-% ## Build a specific version (e.g. build-2.7)
|
||||||
docker build -t '$(IMAGEWTAG)-python$*' -f Dockerfile-$* .;
|
docker build -t '$(IMAGEWTAG)-python$*' -f Dockerfile-$* .;
|
||||||
@@ -75,7 +76,7 @@ pip_upload: pip_test ## Upload package to pip
|
|||||||
|
|
||||||
push-latest: $(addprefix push-latest-,$(PYVERSIONS)) ## Push the "latest" tag to dockerhub
|
push-latest: $(addprefix push-latest-,$(PYVERSIONS)) ## Push the "latest" tag to dockerhub
|
||||||
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGEWTAG)'
|
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGEWTAG)'
|
||||||
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGENAME)'
|
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGENAME):latest'
|
||||||
docker push '$(IMAGENAME):latest'
|
docker push '$(IMAGENAME):latest'
|
||||||
docker push '$(IMAGEWTAG)'
|
docker push '$(IMAGEWTAG)'
|
||||||
|
|
||||||
|
@@ -6,8 +6,6 @@ RUN apt-get update && apt-get install -y \
|
|||||||
libblas-dev liblapack-dev liblapacke-dev gfortran \
|
libblas-dev liblapack-dev liblapacke-dev gfortran \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN pip install --no-cache-dir --upgrade numpy scipy scikit-learn
|
|
||||||
|
|
||||||
RUN mkdir /cache/ /senpy-plugins /data/
|
RUN mkdir /cache/ /senpy-plugins /data/
|
||||||
|
|
||||||
VOLUME /data/
|
VOLUME /data/
|
||||||
@@ -20,8 +18,8 @@ ONBUILD WORKDIR /senpy-plugins/
|
|||||||
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY test-requirements.txt requirements.txt /usr/src/app/
|
COPY test-requirements.txt requirements.txt extra-requirements.txt /usr/src/app/
|
||||||
RUN pip install --no-cache-dir --use-wheel -r test-requirements.txt -r requirements.txt
|
RUN pip install --no-cache-dir -r test-requirements.txt -r requirements.txt -r extra-requirements.txt
|
||||||
COPY . /usr/src/app/
|
COPY . /usr/src/app/
|
||||||
RUN pip install --no-cache-dir --no-index --no-deps --editable .
|
RUN pip install --no-cache-dir --no-index --no-deps --editable .
|
||||||
|
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
|
10
docker-compose.dev.yml
Normal file
10
docker-compose.dev.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
senpy:
|
||||||
|
image: "${IMAGENAME-gsiupm/senpy}:${VERSION-latest}"
|
||||||
|
entrypoint: ["/bin/bash"]
|
||||||
|
working_dir: "/senpy-plugins"
|
||||||
|
ports:
|
||||||
|
- 5000:5000
|
||||||
|
volumes:
|
||||||
|
- ".:/usr/src/app/"
|
9
docker-compose.test.yml
Normal file
9
docker-compose.test.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: "${IMAGENAME-gsiupm/senpy}:${VERSION-dev}"
|
||||||
|
entrypoint: ["py.test"]
|
||||||
|
volumes:
|
||||||
|
- ".:/usr/src/app/"
|
||||||
|
command:
|
||||||
|
[]
|
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
senpy:
|
||||||
|
image: "${IMAGENAME-gsiupm/senpy}:${VERSION-dev}"
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile${PYVERSION--2.7}
|
||||||
|
ports:
|
||||||
|
- 5001:5000
|
||||||
|
volumes:
|
||||||
|
- "./data:/data"
|
@@ -6,13 +6,9 @@
|
|||||||
],
|
],
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"@type": [
|
|
||||||
"nif:RFC5147String",
|
|
||||||
"nif:Context"
|
|
||||||
],
|
|
||||||
"nif:beginIndex": 0,
|
"nif:beginIndex": 0,
|
||||||
"nif:endIndex": 40,
|
"nif:endIndex": 40,
|
||||||
"nif:isString": "My favourite actress is Natalie Portman"
|
"text": "An entry should have a nif:isString key"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -3,10 +3,21 @@
|
|||||||
"@id": "me:Result1",
|
"@id": "me:Result1",
|
||||||
"@type": "results",
|
"@type": "results",
|
||||||
"analysis": [
|
"analysis": [
|
||||||
"me:SAnalysis1",
|
{
|
||||||
"me:SgAnalysis1",
|
"@id": "_:SAnalysis1_Activity",
|
||||||
"me:EmotionAnalysis1",
|
"@type": "marl:SentimentAnalysis",
|
||||||
"me:NER1"
|
"prov:wasAssociatedWith": "me:SAnalysis1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@id": "_:EmotionAnalysis1_Activity",
|
||||||
|
"@type": "onyx:EmotionAnalysis",
|
||||||
|
"prov:wasAssociatedWith": "me:EmotionAnalysis1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@id": "_:NER1_Activity",
|
||||||
|
"@type": "me:NER",
|
||||||
|
"prov:wasAssociatedWith": "me:NER1"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
@@ -23,7 +34,7 @@
|
|||||||
"nif:endIndex": 13,
|
"nif:endIndex": 13,
|
||||||
"nif:anchorOf": "Microsoft",
|
"nif:anchorOf": "Microsoft",
|
||||||
"me:references": "http://dbpedia.org/page/Microsoft",
|
"me:references": "http://dbpedia.org/page/Microsoft",
|
||||||
"prov:wasGeneratedBy": "me:NER1"
|
"prov:wasGeneratedBy": "_:NER1_Activity"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@id": "http://micro.blog/status1#char=25,37",
|
"@id": "http://micro.blog/status1#char=25,37",
|
||||||
@@ -31,7 +42,7 @@
|
|||||||
"nif:endIndex": 37,
|
"nif:endIndex": 37,
|
||||||
"nif:anchorOf": "Windows Phone",
|
"nif:anchorOf": "Windows Phone",
|
||||||
"me:references": "http://dbpedia.org/page/Windows_Phone",
|
"me:references": "http://dbpedia.org/page/Windows_Phone",
|
||||||
"prov:wasGeneratedBy": "me:NER1"
|
"prov:wasGeneratedBy": "_:NER1_Activity"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"suggestions": [
|
"suggestions": [
|
||||||
@@ -40,7 +51,7 @@
|
|||||||
"nif:beginIndex": 16,
|
"nif:beginIndex": 16,
|
||||||
"nif:endIndex": 77,
|
"nif:endIndex": 77,
|
||||||
"nif:anchorOf": "put your Windows Phone on your newest #open technology program",
|
"nif:anchorOf": "put your Windows Phone on your newest #open technology program",
|
||||||
"prov:wasGeneratedBy": "me:SgAnalysis1"
|
"prov:wasGeneratedBy": "_:SgAnalysis1_Activity"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sentiments": [
|
"sentiments": [
|
||||||
@@ -51,14 +62,14 @@
|
|||||||
"nif:anchorOf": "You'll be awesome.",
|
"nif:anchorOf": "You'll be awesome.",
|
||||||
"marl:hasPolarity": "marl:Positive",
|
"marl:hasPolarity": "marl:Positive",
|
||||||
"marl:polarityValue": 0.9,
|
"marl:polarityValue": 0.9,
|
||||||
"prov:wasGeneratedBy": "me:SAnalysis1"
|
"prov:wasGeneratedBy": "_:SgAnalysis1_Activity"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"emotions": [
|
"emotions": [
|
||||||
{
|
{
|
||||||
"@id": "http://micro.blog/status1#char=0,109",
|
"@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",
|
"nif:anchorOf": "Dear Microsoft, put your Windows Phone on your newest #open technology program. You'll be awesome. #opensource",
|
||||||
"prov:wasGeneratedBy": "me:EAnalysis1",
|
"prov:wasGeneratedBy": "_:EmotionAnalysis1_Activity",
|
||||||
"onyx:hasEmotion": [
|
"onyx:hasEmotion": [
|
||||||
{
|
{
|
||||||
"onyx:hasEmotionCategory": "wna:liking"
|
"onyx:hasEmotionCategory": "wna:liking"
|
||||||
|
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@@ -1,19 +1,18 @@
|
|||||||
{
|
{
|
||||||
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
|
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
|
||||||
"@id": "http://example.com#NIFExample",
|
"@id": "me:Result1",
|
||||||
"@type": "results",
|
"@type": "results",
|
||||||
"analysis": [
|
"analysis": [ ],
|
||||||
],
|
"entries": [
|
||||||
"entries": [
|
{
|
||||||
{
|
"@id": "http://example.org#char=0,40",
|
||||||
"@id": "http://example.org#char=0,40",
|
"@type": [
|
||||||
"@type": [
|
"nif:RFC5147String",
|
||||||
"nif:RFC5147String",
|
"nif:Context"
|
||||||
"nif:Context"
|
],
|
||||||
],
|
"nif:beginIndex": 0,
|
||||||
"nif:beginIndex": 0,
|
"nif:endIndex": 40,
|
||||||
"nif:endIndex": 40,
|
"nif:isString": "My favourite actress is Natalie Portman"
|
||||||
"nif:isString": "My favourite actress is Natalie Portman"
|
}
|
||||||
}
|
]
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
@@ -1,88 +1,100 @@
|
|||||||
{
|
{
|
||||||
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
|
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
|
||||||
"@id": "me:Result1",
|
"@id": "me:Result1",
|
||||||
"@type": "results",
|
"@type": "results",
|
||||||
"analysis": [
|
"analysis": [
|
||||||
{
|
|
||||||
"@id": "me:SAnalysis1",
|
|
||||||
"@type": "marl:SentimentAnalysis",
|
|
||||||
"marl:maxPolarityValue": 1,
|
|
||||||
"marl:minPolarityValue": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@id": "me:SgAnalysis1",
|
|
||||||
"@type": "me:SuggestionAnalysis"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@id": "me:EmotionAnalysis1",
|
|
||||||
"@type": "me:EmotionAnalysis"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@id": "me:NER1",
|
|
||||||
"@type": "me:NER"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"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",
|
"@id": "_:SAnalysis1_Activity",
|
||||||
"nif:beginIndex": 5,
|
"@type": "marl:SentimentAnalysis",
|
||||||
"nif:endIndex": 13,
|
"prov:wasAssociatedWith": "me:SentimentAnalysis",
|
||||||
"nif:anchorOf": "Microsoft",
|
"prov:used": [
|
||||||
"me:references": "http://dbpedia.org/page/Microsoft",
|
{
|
||||||
"prov:wasGeneratedBy": "me:NER1"
|
"name": "marl:maxPolarityValue",
|
||||||
|
"prov:value": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "marl:minPolarityValue",
|
||||||
|
"prov:value": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@id": "http://micro.blog/status1#char=25,37",
|
"@id": "_:SgAnalysis1_Activity",
|
||||||
"nif:beginIndex": 25,
|
"prov:wasAssociatedWith": "me:SgAnalysis1",
|
||||||
"nif:endIndex": 37,
|
"@type": "me:SuggestionAnalysis"
|
||||||
"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",
|
"@id": "_:EmotionAnalysis1_Activity",
|
||||||
"nif:beginIndex": 16,
|
"@type": "me:EmotionAnalysis",
|
||||||
"nif:endIndex": 77,
|
"prov:wasAssociatedWith": "me:EmotionAnalysis1"
|
||||||
"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",
|
"@id": "_:NER1_Activity",
|
||||||
"nif:beginIndex": 80,
|
"@type": "me:NER",
|
||||||
"nif:endIndex": 97,
|
"prov:wasAssociatedWith": "me:EmotionNER1"
|
||||||
"nif:anchorOf": "You'll be awesome.",
|
|
||||||
"marl:hasPolarity": "marl:Positive",
|
|
||||||
"marl:polarityValue": 0.9,
|
|
||||||
"prov:wasGeneratedBy": "me:SAnalysis1"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"emotions": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"@id": "http://micro.blog/status1#char=0,109",
|
"@id": "http://micro.blog/status1",
|
||||||
"nif:anchorOf": "Dear Microsoft, put your Windows Phone on your newest #open technology program. You'll be awesome. #opensource",
|
"@type": [
|
||||||
"prov:wasGeneratedBy": "me:EAnalysis1",
|
"nif:RFC5147String",
|
||||||
"onyx:hasEmotion": [
|
"nif:Context"
|
||||||
{
|
],
|
||||||
"onyx:hasEmotionCategory": "wna:liking"
|
"nif:isString": "Dear Microsoft, put your Windows Phone on your newest #open technology program. You'll be awesome. #opensource",
|
||||||
},
|
"entities": [
|
||||||
{
|
{
|
||||||
"onyx:hasEmotionCategory": "wna:excitement"
|
"@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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
@@ -4,8 +4,9 @@
|
|||||||
"@type": "results",
|
"@type": "results",
|
||||||
"analysis": [
|
"analysis": [
|
||||||
{
|
{
|
||||||
"@id": "me:EmotionAnalysis1",
|
"@id": "me:EmotionAnalysis1_Activity",
|
||||||
"@type": "onyx:EmotionAnalysis"
|
"@type": "me:EmotionAnalysis1",
|
||||||
|
"prov:wasAssociatedWith": "me:EmotionAnalysis1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"entries": [
|
"entries": [
|
||||||
@@ -26,7 +27,7 @@
|
|||||||
{
|
{
|
||||||
"@id": "http://micro.blog/status1#char=0,109",
|
"@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",
|
"nif:anchorOf": "Dear Microsoft, put your Windows Phone on your newest #open technology program. You'll be awesome. #opensource",
|
||||||
"prov:wasGeneratedBy": "me:EmotionAnalysis1",
|
"prov:wasGeneratedBy": "_:EmotionAnalysis1_Activity",
|
||||||
"onyx:hasEmotion": [
|
"onyx:hasEmotion": [
|
||||||
{
|
{
|
||||||
"onyx:hasEmotionCategory": "wna:liking"
|
"onyx:hasEmotionCategory": "wna:liking"
|
||||||
|
@@ -4,8 +4,9 @@
|
|||||||
"@type": "results",
|
"@type": "results",
|
||||||
"analysis": [
|
"analysis": [
|
||||||
{
|
{
|
||||||
"@id": "me:NER1",
|
"@id": "_:NER1_Activity",
|
||||||
"@type": "me:NERAnalysis"
|
"@type": "me:NERAnalysis",
|
||||||
|
"prov:wasAssociatedWith": "me:NER1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"entries": [
|
"entries": [
|
||||||
|
@@ -9,9 +9,15 @@
|
|||||||
"@type": "results",
|
"@type": "results",
|
||||||
"analysis": [
|
"analysis": [
|
||||||
{
|
{
|
||||||
"@id": "me:HesamsAnalysis",
|
"@id": "me:HesamsAnalysis_Activity",
|
||||||
"@type": "onyx:EmotionAnalysis",
|
"@type": "onyx:EmotionAnalysis",
|
||||||
"onyx:usesEmotionModel": "emovoc:pad-dimensions"
|
"prov:wasAssociatedWith": "me:HesamsAnalysis",
|
||||||
|
"prov:used": [
|
||||||
|
{
|
||||||
|
"name": "emotion-model",
|
||||||
|
"prov:value": "emovoc:pad-dimensions"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"entries": [
|
"entries": [
|
||||||
@@ -32,7 +38,7 @@
|
|||||||
{
|
{
|
||||||
"@id": "Entry1#char=0,21",
|
"@id": "Entry1#char=0,21",
|
||||||
"nif:anchorOf": "This is a test string",
|
"nif:anchorOf": "This is a test string",
|
||||||
"prov:wasGeneratedBy": "me:HesamAnalysis",
|
"prov:wasGeneratedBy": "_:HesamAnalysis_Activity",
|
||||||
"onyx:hasEmotion": [
|
"onyx:hasEmotion": [
|
||||||
{
|
{
|
||||||
"emovoc:pleasure": 0.5,
|
"emovoc:pleasure": 0.5,
|
||||||
|
@@ -4,10 +4,9 @@
|
|||||||
"@type": "results",
|
"@type": "results",
|
||||||
"analysis": [
|
"analysis": [
|
||||||
{
|
{
|
||||||
"@id": "me:SAnalysis1",
|
"@id": "_:SAnalysis1_Activity",
|
||||||
"@type": "marl:SentimentAnalysis",
|
"@type": "marl:SentimentAnalysis",
|
||||||
"marl:maxPolarityValue": 1,
|
"prov:wasAssociatedWith": "me:SAnalysis1"
|
||||||
"marl:minPolarityValue": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"entries": [
|
"entries": [
|
||||||
@@ -30,7 +29,7 @@
|
|||||||
"nif:anchorOf": "You'll be awesome.",
|
"nif:anchorOf": "You'll be awesome.",
|
||||||
"marl:hasPolarity": "marl:Positive",
|
"marl:hasPolarity": "marl:Positive",
|
||||||
"marl:polarityValue": 0.9,
|
"marl:polarityValue": 0.9,
|
||||||
"prov:wasGeneratedBy": "me:SAnalysis1"
|
"prov:wasGeneratedBy": "_:SAnalysis1_Activity"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"emotionSets": [
|
"emotionSets": [
|
||||||
|
@@ -3,7 +3,11 @@
|
|||||||
"@id": "me:Result1",
|
"@id": "me:Result1",
|
||||||
"@type": "results",
|
"@type": "results",
|
||||||
"analysis": [
|
"analysis": [
|
||||||
"me:SgAnalysis1"
|
{
|
||||||
|
"@id": "_:SgAnalysis1_Activity",
|
||||||
|
"@type": "me:SuggestionAnalysis",
|
||||||
|
"prov:wasAssociatedWith": "me:SgAnalysis1"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
@@ -12,7 +16,6 @@
|
|||||||
"nif:RFC5147String",
|
"nif:RFC5147String",
|
||||||
"nif:Context"
|
"nif:Context"
|
||||||
],
|
],
|
||||||
"prov:wasGeneratedBy": "me:SAnalysis1",
|
|
||||||
"nif:isString": "Dear Microsoft, put your Windows Phone on your newest #open technology program. You'll be awesome. #opensource",
|
"nif:isString": "Dear Microsoft, put your Windows Phone on your newest #open technology program. You'll be awesome. #opensource",
|
||||||
"entities": [
|
"entities": [
|
||||||
],
|
],
|
||||||
@@ -22,7 +25,7 @@
|
|||||||
"nif:beginIndex": 16,
|
"nif:beginIndex": 16,
|
||||||
"nif:endIndex": 77,
|
"nif:endIndex": 77,
|
||||||
"nif:anchorOf": "put your Windows Phone on your newest #open technology program",
|
"nif:anchorOf": "put your Windows Phone on your newest #open technology program",
|
||||||
"prov:wasGeneratedBy": "me:SgAnalysis1"
|
"prov:wasGeneratedBy": "_:SgAnalysis1_Activity"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sentiments": [
|
"sentiments": [
|
||||||
|
@@ -1,8 +1,11 @@
|
|||||||
What is Senpy?
|
What is Senpy?
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
Web services can get really complex: data validation, user interaction, formatting, logging., etc.
|
Senpy is a framework for text analysis using Linked Data. There are three main applications of Senpy so far: sentiment and emotion analysis, user profiling and entity recoginition. Annotations and Services are compliant with NIF (NLP Interchange Format).
|
||||||
The figure below summarizes the typical features in an analysis service.
|
|
||||||
|
Senpy aims at providing a framework where analysis modules can be integrated easily as plugins, and providing a core functionality for managing tasks such as data validation, user interaction, formatting, logging, translation to linked data, etc.
|
||||||
|
|
||||||
|
The figure below summarizes the typical features in a text analysis service.
|
||||||
Senpy implements all the common blocks, so developers can focus on what really matters: great analysis algorithms that solve real problems.
|
Senpy implements all the common blocks, so developers can focus on what really matters: great analysis algorithms that solve real problems.
|
||||||
|
|
||||||
.. image:: senpy-framework.png
|
.. image:: senpy-framework.png
|
||||||
|
@@ -1,8 +1,24 @@
|
|||||||
Vocabularies and model
|
Vocabularies and model
|
||||||
======================
|
======================
|
||||||
|
|
||||||
The model used in Senpy is based on the following vocabularies:
|
The model used in Senpy is based on NIF 2.0 [1], which defines a semantic format and API for improving interoperability among natural language processing services.
|
||||||
|
|
||||||
* Marl, a vocabulary designed to annotate and describe subjetive opinions expressed on the web or in information systems.
|
Senpy has been applied to sentiment and emotion analysis services using the following vocabularies:
|
||||||
* Onyx, which is built one the same principles as Marl to annotate and describe emotions, and provides interoperability with Emotion Markup Language.
|
|
||||||
* NIF 2.0, which defines a semantic format and APO for improving interoperability among natural language processing services
|
* Marl [2,6], a vocabulary designed to annotate and describe subjetive opinions expressed on the web or in information systems.
|
||||||
|
* Onyx [3,5], which is built one the same principles as Marl to annotate and describe emotions, and provides interoperability with Emotion Markup Language.
|
||||||
|
|
||||||
|
An overview of the vocabularies and their use can be found in [4].
|
||||||
|
|
||||||
|
|
||||||
|
[1] Guidelines for developing NIF-based NLP services, Final Community Group Report 22 December 2015 Available at: https://www.w3.org/2015/09/bpmlod-reports/nif-based-nlp-webservices/
|
||||||
|
|
||||||
|
[2] Marl Ontology Specification, available at http://www.gsi.dit.upm.es/ontologies/marl/
|
||||||
|
|
||||||
|
[3] Onyx Ontology Specification, available at http://www.gsi.dit.upm.es/ontologies/onyx/
|
||||||
|
|
||||||
|
[4] Iglesias, C. A., Sánchez-Rada, J. F., Vulcu, G., & Buitelaar, P. (2017). Linked Data Models for Sentiment and Emotion Analysis in Social Networks. In Sentiment Analysis in Social Networks (pp. 49-69).
|
||||||
|
|
||||||
|
[5] Sánchez-Rada, J. F., & Iglesias, C. A. (2016). Onyx: A linked data approach to emotion representation. Information Processing & Management, 52(1), 99-114.
|
||||||
|
|
||||||
|
[6] Westerski, A., Iglesias Fernandez, C. A., & Tapia Rico, F. (2011). Linked opinions: Describing sentiments on the structured web of data.
|
||||||
|
@@ -11,7 +11,7 @@ class Async(AnalysisPlugin):
|
|||||||
'''An example of an asynchronous module'''
|
'''An example of an asynchronous module'''
|
||||||
author = '@balkian'
|
author = '@balkian'
|
||||||
version = '0.2'
|
version = '0.2'
|
||||||
async = True
|
sync = False
|
||||||
|
|
||||||
def _do_async(self, num_processes):
|
def _do_async(self, num_processes):
|
||||||
pool = multiprocessing.Pool(processes=num_processes)
|
pool = multiprocessing.Pool(processes=num_processes)
|
||||||
|
@@ -18,7 +18,7 @@ class BasicBox(SentimentBox):
|
|||||||
'default': 'marl:Neutral'
|
'default': 'marl:Neutral'
|
||||||
}
|
}
|
||||||
|
|
||||||
def predict(self, input):
|
def predict_one(self, input):
|
||||||
output = basic.get_polarity(input)
|
output = basic.get_polarity(input)
|
||||||
return self.mappings.get(output, self.mappings['default'])
|
return self.mappings.get(output, self.mappings['default'])
|
||||||
|
|
||||||
|
@@ -18,7 +18,7 @@ class Basic(MappingMixin, SentimentBox):
|
|||||||
'default': 'marl:Neutral'
|
'default': 'marl:Neutral'
|
||||||
}
|
}
|
||||||
|
|
||||||
def predict(self, input):
|
def predict_one(self, input):
|
||||||
return basic.get_polarity(input)
|
return basic.get_polarity(input)
|
||||||
|
|
||||||
test_cases = [{
|
test_cases = [{
|
||||||
|
@@ -43,7 +43,6 @@ class Dictionary(plugins.SentimentPlugin):
|
|||||||
|
|
||||||
class EmojiOnly(Dictionary):
|
class EmojiOnly(Dictionary):
|
||||||
'''Sentiment annotation with a basic lexicon of emojis'''
|
'''Sentiment annotation with a basic lexicon of emojis'''
|
||||||
description = 'A plugin'
|
|
||||||
dictionaries = [basic.emojis]
|
dictionaries = [basic.emojis]
|
||||||
|
|
||||||
test_cases = [{
|
test_cases = [{
|
||||||
|
@@ -18,7 +18,7 @@ class PipelineSentiment(MappingMixin, SentimentBox):
|
|||||||
-1: 'marl:Negative'
|
-1: 'marl:Negative'
|
||||||
}
|
}
|
||||||
|
|
||||||
def predict(self, input):
|
def predict_one(self, input):
|
||||||
return pipeline.predict([input, ])[0]
|
return pipeline.predict([input, ])[0]
|
||||||
|
|
||||||
test_cases = [
|
test_cases = [
|
||||||
|
1
extra-requirements.txt
Normal file
1
extra-requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
gsitk
|
@@ -9,3 +9,7 @@ jsonref
|
|||||||
PyYAML
|
PyYAML
|
||||||
rdflib
|
rdflib
|
||||||
rdflib-jsonld
|
rdflib-jsonld
|
||||||
|
numpy
|
||||||
|
scipy
|
||||||
|
scikit-learn
|
||||||
|
responses
|
||||||
|
@@ -22,6 +22,7 @@ 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
|
||||||
@@ -39,7 +40,7 @@ def main():
|
|||||||
'-l',
|
'-l',
|
||||||
metavar='logging_level',
|
metavar='logging_level',
|
||||||
type=str,
|
type=str,
|
||||||
default="ERROR",
|
default="WARN",
|
||||||
help='Logging level')
|
help='Logging level')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--debug',
|
'--debug',
|
||||||
@@ -75,6 +76,17 @@ 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',
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
help='Do not run a server, just test all plugins')
|
||||||
|
parser.add_argument(
|
||||||
|
'--test',
|
||||||
|
'-t',
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
help='Test all plugins before launching the server')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--only-list',
|
'--only-list',
|
||||||
'--list',
|
'--list',
|
||||||
@@ -92,12 +104,24 @@ def main():
|
|||||||
action='store_false',
|
action='store_false',
|
||||||
default=True,
|
default=True,
|
||||||
help='Run a threaded server')
|
help='Run a threaded server')
|
||||||
|
parser.add_argument(
|
||||||
|
'--no-deps',
|
||||||
|
'-n',
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
help='Skip installing dependencies')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--version',
|
'--version',
|
||||||
'-v',
|
'-v',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
default=False,
|
default=False,
|
||||||
help='Output the senpy version and exit')
|
help='Output the senpy version and exit')
|
||||||
|
parser.add_argument(
|
||||||
|
'--allow-fail',
|
||||||
|
'--fail',
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
help='Do not exit if some plugins fail to activate')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if args.version:
|
if args.version:
|
||||||
print('Senpy version {}'.format(senpy.__version__))
|
print('Senpy version {}'.format(senpy.__version__))
|
||||||
@@ -112,16 +136,27 @@ def main():
|
|||||||
data_folder=args.data_folder)
|
data_folder=args.data_folder)
|
||||||
if args.only_list:
|
if args.only_list:
|
||||||
plugins = sp.plugins()
|
plugins = sp.plugins()
|
||||||
maxwidth = max(len(x.id) for x in plugins)
|
maxname = max(len(x.name) for x in plugins)
|
||||||
|
maxversion = max(len(x.version) for x in plugins)
|
||||||
|
print('Found {} plugins:'.format(len(plugins)))
|
||||||
for plugin in plugins:
|
for plugin in plugins:
|
||||||
import inspect
|
import inspect
|
||||||
fpath = inspect.getfile(plugin.__class__)
|
fpath = inspect.getfile(plugin.__class__)
|
||||||
print('{: <{width}} @ {}'.format(plugin.id, fpath, width=maxwidth))
|
print('\t{: <{maxname}} @ {: <{maxversion}} -> {}'.format(plugin.name,
|
||||||
|
plugin.version,
|
||||||
|
fpath,
|
||||||
|
maxname=maxname,
|
||||||
|
maxversion=maxversion))
|
||||||
return
|
return
|
||||||
sp.install_deps()
|
if not args.no_deps:
|
||||||
|
sp.install_deps()
|
||||||
if args.only_install:
|
if args.only_install:
|
||||||
return
|
return
|
||||||
sp.activate_all()
|
sp.activate_all(allow_fail=args.allow_fail)
|
||||||
|
if args.test or args.only_test:
|
||||||
|
easy_test(sp.plugins(), debug=args.debug)
|
||||||
|
if args.only_test:
|
||||||
|
return
|
||||||
print('Senpy version {}'.format(senpy.__version__))
|
print('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))
|
||||||
|
176
senpy/api.py
176
senpy/api.py
@@ -1,26 +1,29 @@
|
|||||||
from future.utils import iteritems
|
from future.utils import iteritems
|
||||||
from .models import Error, Results, Entry, from_string
|
from .models import Analysis, Error, Results, Entry, from_string
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
boolean = [True, False]
|
||||||
|
|
||||||
API_PARAMS = {
|
API_PARAMS = {
|
||||||
"algorithm": {
|
"algorithm": {
|
||||||
"aliases": ["algorithms", "a", "algo"],
|
"aliases": ["algorithms", "a", "algo"],
|
||||||
"required": False,
|
"required": True,
|
||||||
|
"default": 'default',
|
||||||
"description": ("Algorithms that will be used to process the request."
|
"description": ("Algorithms that will be used to process the request."
|
||||||
"It may be a list of comma-separated names."),
|
"It may be a list of comma-separated names."),
|
||||||
},
|
},
|
||||||
"expanded-jsonld": {
|
"expanded-jsonld": {
|
||||||
"@id": "expanded-jsonld",
|
"@id": "expanded-jsonld",
|
||||||
"aliases": ["expanded"],
|
"aliases": ["expanded"],
|
||||||
"options": "boolean",
|
"options": boolean,
|
||||||
"required": True,
|
"required": True,
|
||||||
"default": False
|
"default": False
|
||||||
},
|
},
|
||||||
"with_parameters": {
|
"with_parameters": {
|
||||||
"aliases": ['withparameters',
|
"aliases": ['withparameters',
|
||||||
'with-parameters'],
|
'with-parameters'],
|
||||||
"options": "boolean",
|
"options": boolean,
|
||||||
"default": False,
|
"default": False,
|
||||||
"required": True
|
"required": True
|
||||||
},
|
},
|
||||||
@@ -29,16 +32,24 @@ API_PARAMS = {
|
|||||||
"aliases": ["o"],
|
"aliases": ["o"],
|
||||||
"default": "json-ld",
|
"default": "json-ld",
|
||||||
"required": True,
|
"required": True,
|
||||||
"options": ["json-ld", "turtle"],
|
"options": ["json-ld", "turtle", "ntriples"],
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"@id": "help",
|
"@id": "help",
|
||||||
"description": "Show additional help to know more about the possible parameters",
|
"description": "Show additional help to know more about the possible parameters",
|
||||||
"aliases": ["h"],
|
"aliases": ["h"],
|
||||||
"required": True,
|
"required": True,
|
||||||
"options": "boolean",
|
"options": boolean,
|
||||||
"default": False
|
"default": False
|
||||||
},
|
},
|
||||||
|
"verbose": {
|
||||||
|
"@id": "verbose",
|
||||||
|
"description": "Show all help, including the common API parameters, or only plugin-related info",
|
||||||
|
"aliases": ["v"],
|
||||||
|
"required": True,
|
||||||
|
"options": boolean,
|
||||||
|
"default": True
|
||||||
|
},
|
||||||
"emotionModel": {
|
"emotionModel": {
|
||||||
"@id": "emotionModel",
|
"@id": "emotionModel",
|
||||||
"aliases": ["emoModel"],
|
"aliases": ["emoModel"],
|
||||||
@@ -53,6 +64,21 @@ API_PARAMS = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EVAL_PARAMS = {
|
||||||
|
"algorithm": {
|
||||||
|
"aliases": ["plug", "p", "plugins", "algorithms", 'algo', 'a', 'plugin'],
|
||||||
|
"description": "Plugins to be evaluated",
|
||||||
|
"required": True,
|
||||||
|
"help": "See activated plugins in /plugins"
|
||||||
|
},
|
||||||
|
"dataset": {
|
||||||
|
"aliases": ["datasets", "data", "d"],
|
||||||
|
"description": "Datasets to be evaluated",
|
||||||
|
"required": True,
|
||||||
|
"help": "See avalaible datasets in /datasets"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PLUGINS_PARAMS = {
|
PLUGINS_PARAMS = {
|
||||||
"plugin_type": {
|
"plugin_type": {
|
||||||
"@id": "pluginType",
|
"@id": "pluginType",
|
||||||
@@ -68,7 +94,7 @@ WEB_PARAMS = {
|
|||||||
"aliases": ["headers"],
|
"aliases": ["headers"],
|
||||||
"required": True,
|
"required": True,
|
||||||
"default": False,
|
"default": False,
|
||||||
"options": "boolean"
|
"options": boolean
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,10 +143,19 @@ NIF_PARAMS = {
|
|||||||
"aliases": ["u"],
|
"aliases": ["u"],
|
||||||
"required": False,
|
"required": False,
|
||||||
"default": "RFC5147String",
|
"default": "RFC5147String",
|
||||||
"options": "RFC5147String"
|
"options": ["RFC5147String", ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BUILTIN_PARAMS = {}
|
||||||
|
|
||||||
|
for d in [
|
||||||
|
NIF_PARAMS, CLI_PARAMS, WEB_PARAMS, PLUGINS_PARAMS, EVAL_PARAMS,
|
||||||
|
API_PARAMS
|
||||||
|
]:
|
||||||
|
for k, v in d.items():
|
||||||
|
BUILTIN_PARAMS[k] = v
|
||||||
|
|
||||||
|
|
||||||
def parse_params(indict, *specs):
|
def parse_params(indict, *specs):
|
||||||
if not specs:
|
if not specs:
|
||||||
@@ -132,7 +167,7 @@ def parse_params(indict, *specs):
|
|||||||
for param, options in iteritems(spec):
|
for param, options in iteritems(spec):
|
||||||
for alias in options.get("aliases", []):
|
for alias in options.get("aliases", []):
|
||||||
# Replace each alias with the correct name of the parameter
|
# Replace each alias with the correct name of the parameter
|
||||||
if alias in indict and alias is not param:
|
if alias in indict and alias != param:
|
||||||
outdict[param] = indict[alias]
|
outdict[param] = indict[alias]
|
||||||
del outdict[alias]
|
del outdict[alias]
|
||||||
continue
|
continue
|
||||||
@@ -142,10 +177,9 @@ def parse_params(indict, *specs):
|
|||||||
outdict[param] = options["default"]
|
outdict[param] = options["default"]
|
||||||
elif options.get("required", False):
|
elif options.get("required", False):
|
||||||
wrong_params[param] = spec[param]
|
wrong_params[param] = spec[param]
|
||||||
continue
|
elif "options" in options:
|
||||||
if "options" in options:
|
if options["options"] == boolean:
|
||||||
if options["options"] == "boolean":
|
outdict[param] = str(outdict[param]).lower() in ['true', '1']
|
||||||
outdict[param] = outdict[param] in [None, True, 'true', '1']
|
|
||||||
elif outdict[param] not in options["options"]:
|
elif outdict[param] not in options["options"]:
|
||||||
wrong_params[param] = spec[param]
|
wrong_params[param] = spec[param]
|
||||||
if wrong_params:
|
if wrong_params:
|
||||||
@@ -156,30 +190,126 @@ def parse_params(indict, *specs):
|
|||||||
parameters=outdict,
|
parameters=outdict,
|
||||||
errors=wrong_params)
|
errors=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):
|
def get_all_params(plugins, *specs):
|
||||||
params = request.parameters.copy()
|
'''Return a list of parameters for a given set of specifications and plugins.'''
|
||||||
if plugin:
|
dic = {}
|
||||||
extra_params = parse_params(params, plugin.get('extra_params', {}))
|
for s in specs:
|
||||||
params.update(extra_params)
|
dic.update(s)
|
||||||
|
dic.update(get_extra_params(plugins))
|
||||||
|
return dic
|
||||||
|
|
||||||
|
|
||||||
|
def get_extra_params(plugins):
|
||||||
|
'''Get a list of possible parameters given a list of plugins'''
|
||||||
|
params = {}
|
||||||
|
extra_params = {}
|
||||||
|
for plugin in plugins:
|
||||||
|
this_params = plugin.get('extra_params', {})
|
||||||
|
for k, v in this_params.items():
|
||||||
|
if k not in extra_params:
|
||||||
|
extra_params[k] = {}
|
||||||
|
extra_params[k][plugin.name] = v
|
||||||
|
for k, v in extra_params.items(): # Resolve conflicts
|
||||||
|
if len(v) == 1: # Add the extra options that do not collide
|
||||||
|
params[k] = list(v.values())[0]
|
||||||
|
else:
|
||||||
|
required = False
|
||||||
|
aliases = None
|
||||||
|
options = None
|
||||||
|
default = None
|
||||||
|
nodefault = False # Set when defaults are not compatible
|
||||||
|
|
||||||
|
for plugin, opt in v.items():
|
||||||
|
params['{}.{}'.format(plugin, k)] = opt
|
||||||
|
required = required or opt.get('required', False)
|
||||||
|
newaliases = set(opt.get('aliases', []))
|
||||||
|
if aliases is None:
|
||||||
|
aliases = newaliases
|
||||||
|
else:
|
||||||
|
aliases = aliases & newaliases
|
||||||
|
if 'options' in opt:
|
||||||
|
newoptions = set(opt['options'])
|
||||||
|
options = newoptions if options is None else options & newoptions
|
||||||
|
if 'default' in opt:
|
||||||
|
newdefault = opt['default']
|
||||||
|
if newdefault:
|
||||||
|
if default is None and not nodefault:
|
||||||
|
default = newdefault
|
||||||
|
elif newdefault != default:
|
||||||
|
nodefault = True
|
||||||
|
default = None
|
||||||
|
# Check for incompatibilities
|
||||||
|
if options != set():
|
||||||
|
params[k] = {
|
||||||
|
'default': default,
|
||||||
|
'aliases': list(aliases),
|
||||||
|
'required': required,
|
||||||
|
'options': list(options)
|
||||||
|
}
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def parse_analysis(params, plugins):
|
||||||
|
'''
|
||||||
|
Parse the given parameters individually for each plugin, and get a list of the parameters that
|
||||||
|
belong to each of the plugins. Each item can then be used in the plugin.analyse_entries method.
|
||||||
|
'''
|
||||||
|
analysis_list = []
|
||||||
|
for i, plugin in enumerate(plugins):
|
||||||
|
if not plugin:
|
||||||
|
continue
|
||||||
|
this_params = filter_params(params, plugin, i)
|
||||||
|
parsed = parse_params(this_params, plugin.get('extra_params', {}))
|
||||||
|
analysis = plugin.activity(parsed)
|
||||||
|
analysis_list.append(analysis)
|
||||||
|
return analysis_list
|
||||||
|
|
||||||
|
|
||||||
|
def filter_params(params, plugin, ith=-1):
|
||||||
|
'''
|
||||||
|
Get the values within params that apply to a plugin.
|
||||||
|
More specific names override more general names, in this order:
|
||||||
|
|
||||||
|
<index_order>.parameter > <plugin.name>.parameter > parameter
|
||||||
|
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
>>> filter_params({'0.hello': True, 'hello': False}, Plugin(), 0)
|
||||||
|
{ '0.hello': True, 'hello': True}
|
||||||
|
|
||||||
|
'''
|
||||||
|
thisparams = {}
|
||||||
|
if ith >= 0:
|
||||||
|
ith = '{}.'.format(ith)
|
||||||
|
else:
|
||||||
|
ith = ""
|
||||||
|
for k, v in params.items():
|
||||||
|
if ith and k.startswith(str(ith)):
|
||||||
|
thisparams[k[len(ith):]] = v
|
||||||
|
elif k.startswith(plugin.name):
|
||||||
|
thisparams[k[len(plugin.name) + 1:]] = v
|
||||||
|
elif k not in thisparams:
|
||||||
|
thisparams[k] = v
|
||||||
|
return thisparams
|
||||||
|
|
||||||
|
|
||||||
def parse_call(params):
|
def parse_call(params):
|
||||||
'''Return a results object based on the parameters used in a call/request.
|
'''
|
||||||
|
Return a results object based on the parameters used in a call/request.
|
||||||
'''
|
'''
|
||||||
params = parse_params(params, NIF_PARAMS)
|
params = parse_params(params, NIF_PARAMS)
|
||||||
if params['informat'] == 'text':
|
if params['informat'] == 'text':
|
||||||
results = Results()
|
results = Results()
|
||||||
entry = Entry(nif__isString=params['input'])
|
entry = Entry(nif__isString=params['input'], id='#') # Use @base
|
||||||
results.entries.append(entry)
|
results.entries.append(entry)
|
||||||
elif params['informat'] == 'json-ld':
|
elif params['informat'] == 'json-ld':
|
||||||
results = from_string(params['input'], cls=Results)
|
results = from_string(params['input'], cls=Results)
|
||||||
else: # pragma: no cover
|
else: # pragma: no cover
|
||||||
raise NotImplementedError('Informat {} is not implemented'.format(params['informat']))
|
raise NotImplementedError('Informat {} is not implemented'.format(
|
||||||
|
params['informat']))
|
||||||
results.parameters = params
|
results.parameters = params
|
||||||
return results
|
return results
|
||||||
|
@@ -18,15 +18,15 @@
|
|||||||
Blueprints for Senpy
|
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, redirect)
|
||||||
from .models import Error, Response, Help, Plugins, read_schema
|
from .models import Error, Response, Help, Plugins, read_schema, dump_schema, Datasets
|
||||||
from . import api
|
from . import api
|
||||||
from .version import __version__
|
from .version import __version__
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
|
||||||
import json
|
import json
|
||||||
|
import base64
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -34,6 +34,24 @@ api_blueprint = Blueprint("api", __name__)
|
|||||||
demo_blueprint = Blueprint("demo", __name__, template_folder='templates')
|
demo_blueprint = Blueprint("demo", __name__, template_folder='templates')
|
||||||
ns_blueprint = Blueprint("ns", __name__)
|
ns_blueprint = Blueprint("ns", __name__)
|
||||||
|
|
||||||
|
_mimetypes_r = {'json-ld': ['application/ld+json'],
|
||||||
|
'turtle': ['text/turtle'],
|
||||||
|
'ntriples': ['application/n-triples'],
|
||||||
|
'text': ['text/plain']}
|
||||||
|
|
||||||
|
MIMETYPES = {}
|
||||||
|
|
||||||
|
for k, vs in _mimetypes_r.items():
|
||||||
|
for v in vs:
|
||||||
|
if v in MIMETYPES:
|
||||||
|
raise Exception('MIMETYPE {} specified for two formats: {} and {}'.format(v,
|
||||||
|
v,
|
||||||
|
MIMETYPES[v]))
|
||||||
|
MIMETYPES[v] = k
|
||||||
|
|
||||||
|
DEFAULT_MIMETYPE = 'application/ld+json'
|
||||||
|
DEFAULT_FORMAT = 'json-ld'
|
||||||
|
|
||||||
|
|
||||||
def get_params(req):
|
def get_params(req):
|
||||||
if req.method == 'POST':
|
if req.method == 'POST':
|
||||||
@@ -45,38 +63,76 @@ def get_params(req):
|
|||||||
return indict
|
return indict
|
||||||
|
|
||||||
|
|
||||||
|
def encoded_url(url=None, base=None):
|
||||||
|
code = ''
|
||||||
|
if not url:
|
||||||
|
if request.method == 'GET':
|
||||||
|
url = request.full_path[1:] # Remove the first slash
|
||||||
|
else:
|
||||||
|
hash(frozenset(tuple(request.parameters.items())))
|
||||||
|
code = 'hash:{}'.format(hash)
|
||||||
|
|
||||||
|
code = code or base64.urlsafe_b64encode(url.encode()).decode()
|
||||||
|
|
||||||
|
if base:
|
||||||
|
return base + code
|
||||||
|
return url_for('api.decode', code=code, _external=True)
|
||||||
|
|
||||||
|
|
||||||
|
def decoded_url(code, base=None):
|
||||||
|
if code.startswith('hash:'):
|
||||||
|
raise Exception('Can not decode a URL for a POST request')
|
||||||
|
base = base or request.url_root
|
||||||
|
path = base64.urlsafe_b64decode(code.encode()).decode()
|
||||||
|
return base + path
|
||||||
|
|
||||||
|
|
||||||
@demo_blueprint.route('/')
|
@demo_blueprint.route('/')
|
||||||
def index():
|
def index():
|
||||||
return render_template("index.html", version=__version__)
|
ev = str(get_params(request).get('evaluation', False))
|
||||||
|
evaluation_enabled = ev.lower() not in ['false', 'no', 'none']
|
||||||
|
|
||||||
|
return render_template("index.html",
|
||||||
|
evaluation=evaluation_enabled,
|
||||||
|
version=__version__)
|
||||||
|
|
||||||
|
|
||||||
@api_blueprint.route('/contexts/<entity>.jsonld')
|
@api_blueprint.route('/contexts/<entity>.jsonld')
|
||||||
def context(entity="context"):
|
def context(entity="context"):
|
||||||
context = Response._context
|
context = Response._context
|
||||||
context['@vocab'] = url_for('ns.index', _external=True)
|
context['@vocab'] = url_for('ns.index', _external=True)
|
||||||
|
context['endpoint'] = url_for('api.api_root', _external=True)
|
||||||
return jsonify({"@context": context})
|
return jsonify({"@context": context})
|
||||||
|
|
||||||
|
|
||||||
|
@api_blueprint.route('/d/<code>')
|
||||||
|
def decode(code):
|
||||||
|
try:
|
||||||
|
return redirect(decoded_url(code))
|
||||||
|
except Exception:
|
||||||
|
return Error('invalid URL').flask()
|
||||||
|
|
||||||
|
|
||||||
@ns_blueprint.route('/') # noqa: F811
|
@ns_blueprint.route('/') # noqa: F811
|
||||||
def index():
|
def index():
|
||||||
context = Response._context
|
context = Response._context.copy()
|
||||||
context['@vocab'] = url_for('.ns', _external=True)
|
context['endpoint'] = url_for('api.api_root', _external=True)
|
||||||
return jsonify({"@context": context})
|
return jsonify({"@context": context})
|
||||||
|
|
||||||
|
|
||||||
@api_blueprint.route('/schemas/<schema>')
|
@api_blueprint.route('/schemas/<schema>')
|
||||||
def schema(schema="definitions"):
|
def schema(schema="definitions"):
|
||||||
try:
|
try:
|
||||||
return jsonify(read_schema(schema))
|
return dump_schema(read_schema(schema))
|
||||||
except Exception: # Should be FileNotFoundError, but it's missing from py2
|
except Exception as ex: # Should be FileNotFoundError, but it's missing from py2
|
||||||
return Error(message="Schema not found", status=404).flask()
|
return Error(message="Schema not found: {}".format(ex), status=404).flask()
|
||||||
|
|
||||||
|
|
||||||
def basic_api(f):
|
def basic_api(f):
|
||||||
default_params = {
|
default_params = {
|
||||||
'inHeaders': False,
|
'inHeaders': False,
|
||||||
'expanded-jsonld': False,
|
'expanded-jsonld': False,
|
||||||
'outformat': 'json-ld',
|
'outformat': None,
|
||||||
'with_parameters': True,
|
'with_parameters': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,43 +151,79 @@ def basic_api(f):
|
|||||||
request.parameters = params
|
request.parameters = params
|
||||||
response = f(*args, **kwargs)
|
response = f(*args, **kwargs)
|
||||||
except (Exception) as ex:
|
except (Exception) as ex:
|
||||||
if current_app.debug:
|
if current_app.debug or current_app.config['TESTING']:
|
||||||
raise
|
raise
|
||||||
if not isinstance(ex, Error):
|
if not isinstance(ex, Error):
|
||||||
msg = "{}:\n\t{}".format(ex,
|
msg = "{}".format(ex)
|
||||||
traceback.format_exc())
|
|
||||||
ex = Error(message=msg, status=500)
|
ex = Error(message=msg, status=500)
|
||||||
logger.exception('Error returning analysis result')
|
|
||||||
response = ex
|
response = ex
|
||||||
response.parameters = raw_params
|
response.parameters = raw_params
|
||||||
logger.error(ex)
|
logger.exception(ex)
|
||||||
|
|
||||||
if 'parameters' in response and not params['with_parameters']:
|
if 'parameters' in response and not params['with_parameters']:
|
||||||
del response.parameters
|
del response.parameters
|
||||||
|
|
||||||
logger.info('Response: {}'.format(response))
|
logger.info('Response: {}'.format(response))
|
||||||
|
mime = request.accept_mimetypes\
|
||||||
|
.best_match(MIMETYPES.keys(),
|
||||||
|
DEFAULT_MIMETYPE)
|
||||||
|
|
||||||
|
mimeformat = MIMETYPES.get(mime, DEFAULT_FORMAT)
|
||||||
|
outformat = params['outformat'] or mimeformat
|
||||||
|
|
||||||
return response.flask(
|
return response.flask(
|
||||||
in_headers=params['inHeaders'],
|
in_headers=params['inHeaders'],
|
||||||
headers=headers,
|
headers=headers,
|
||||||
prefix=url_for('.api_root', _external=True),
|
prefix=params.get('prefix', encoded_url()),
|
||||||
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=params['expanded-jsonld'])
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
@api_blueprint.route('/', methods=['POST', 'GET'])
|
@api_blueprint.route('/', defaults={'plugin': None}, methods=['POST', 'GET'])
|
||||||
|
@api_blueprint.route('/<path:plugin>', methods=['POST', 'GET'])
|
||||||
@basic_api
|
@basic_api
|
||||||
def api_root():
|
def api_root(plugin):
|
||||||
|
if plugin:
|
||||||
|
if request.parameters['algorithm'] != api.API_PARAMS['algorithm']['default']:
|
||||||
|
raise Error('You cannot specify the algorithm with a parameter and a URL variable.'
|
||||||
|
' Please, remove one of them')
|
||||||
|
request.parameters['algorithm'] = tuple(plugin.replace('+', '/').split('/'))
|
||||||
|
|
||||||
|
params = request.parameters
|
||||||
|
plugin = request.parameters['algorithm']
|
||||||
|
|
||||||
|
sp = current_app.senpy
|
||||||
|
plugins = sp.get_plugins(plugin)
|
||||||
|
|
||||||
if request.parameters['help']:
|
if request.parameters['help']:
|
||||||
dic = dict(api.API_PARAMS, **api.NIF_PARAMS)
|
apis = []
|
||||||
response = Help(valid_parameters=dic)
|
if request.parameters['verbose']:
|
||||||
|
apis.append(api.BUILTIN_PARAMS)
|
||||||
|
allparameters = api.get_all_params(plugins, *apis)
|
||||||
|
response = Help(valid_parameters=allparameters)
|
||||||
return response
|
return response
|
||||||
req = api.parse_call(request.parameters)
|
req = api.parse_call(request.parameters)
|
||||||
return current_app.senpy.analyse(req)
|
analysis = api.parse_analysis(req.parameters, plugins)
|
||||||
|
results = current_app.senpy.analyse(req, analysis)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
@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'])
|
||||||
@@ -147,6 +239,15 @@ def plugins():
|
|||||||
|
|
||||||
@api_blueprint.route('/plugins/<plugin>/', methods=['POST', 'GET'])
|
@api_blueprint.route('/plugins/<plugin>/', methods=['POST', 'GET'])
|
||||||
@basic_api
|
@basic_api
|
||||||
def plugin(plugin=None):
|
def plugin(plugin):
|
||||||
sp = current_app.senpy
|
sp = current_app.senpy
|
||||||
return sp.get_plugin(plugin)
|
return sp.get_plugin(plugin)
|
||||||
|
|
||||||
|
|
||||||
|
@api_blueprint.route('/datasets/', methods=['POST', 'GET'])
|
||||||
|
@basic_api
|
||||||
|
def datasets():
|
||||||
|
sp = current_app.senpy
|
||||||
|
datasets = sp.datasets
|
||||||
|
dic = Datasets(datasets=list(datasets.values()))
|
||||||
|
return dic
|
||||||
|
@@ -31,10 +31,10 @@ def main_function(argv):
|
|||||||
default_plugins = params.get('default-plugins', False)
|
default_plugins = params.get('default-plugins', False)
|
||||||
sp = Senpy(default_plugins=default_plugins, plugin_folder=plugin_folder)
|
sp = Senpy(default_plugins=default_plugins, plugin_folder=plugin_folder)
|
||||||
request = api.parse_call(params)
|
request = api.parse_call(params)
|
||||||
algos = request.parameters.get('algorithm', None)
|
algos = sp.get_plugins(request.parameters.get('algorithm', None))
|
||||||
if algos:
|
if algos:
|
||||||
for algo in algos:
|
for algo in algos:
|
||||||
sp.activate_plugin(algo)
|
sp.activate_plugin(algo.name)
|
||||||
else:
|
else:
|
||||||
sp.activate_all()
|
sp.activate_all()
|
||||||
res = sp.analyse(request)
|
res = sp.analyse(request)
|
||||||
|
@@ -12,13 +12,24 @@ 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):
|
||||||
|
return self.request('/evaluate', method=method, input=input, **kwargs)
|
||||||
|
|
||||||
def plugins(self, *args, **kwargs):
|
def plugins(self, *args, **kwargs):
|
||||||
resp = self.request(path='/plugins').plugins
|
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.rstrip('/'), path)
|
||||||
response = requests.request(method=method, url=url, params=params)
|
if method == 'POST':
|
||||||
|
response = requests.post(url=url, data=params)
|
||||||
|
else:
|
||||||
|
response = requests.request(method=method, url=url, params=params)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = models.from_dict(response.json())
|
resp = models.from_dict(response.json())
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
@@ -6,8 +6,7 @@ from future import standard_library
|
|||||||
standard_library.install_aliases()
|
standard_library.install_aliases()
|
||||||
|
|
||||||
from . import plugins, api
|
from . import plugins, api
|
||||||
from .plugins import Plugin
|
from .models import Error, AggregatedEvaluation
|
||||||
from .models import Error
|
|
||||||
from .blueprints import api_blueprint, demo_blueprint, ns_blueprint
|
from .blueprints import api_blueprint, demo_blueprint, ns_blueprint
|
||||||
|
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
@@ -16,7 +15,8 @@ import os
|
|||||||
import copy
|
import copy
|
||||||
import errno
|
import errno
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
|
||||||
|
from . import gsitk_compat
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ class Senpy(object):
|
|||||||
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', 'postprocessing'),
|
||||||
from_root=True)
|
from_root=True)
|
||||||
self.app = app
|
self.app = app
|
||||||
if app is not None:
|
if app is not None:
|
||||||
@@ -78,27 +78,47 @@ class Senpy(object):
|
|||||||
def delete_plugin(self, plugin):
|
def delete_plugin(self, plugin):
|
||||||
del self._plugins[plugin.name.lower()]
|
del self._plugins[plugin.name.lower()]
|
||||||
|
|
||||||
def plugins(self, **kwargs):
|
def plugins(self, plugin_type=None, is_activated=True, **kwargs):
|
||||||
""" Return the plugins registered for a given application. Filtered by criteria """
|
""" Return the plugins registered for a given application. Filtered by criteria """
|
||||||
return list(plugins.pfilter(self._plugins, **kwargs))
|
return list(plugins.pfilter(self._plugins, plugin_type=plugin_type,
|
||||||
|
is_activated=is_activated, **kwargs))
|
||||||
|
|
||||||
def get_plugin(self, name, default=None):
|
def get_plugin(self, name, default=None):
|
||||||
if name == 'default':
|
if name == 'default':
|
||||||
return self.default_plugin
|
return self.default_plugin
|
||||||
plugin = name.lower()
|
elif name == 'conversion':
|
||||||
if plugin in self._plugins:
|
return None
|
||||||
return self._plugins[plugin]
|
|
||||||
|
|
||||||
results = self.plugins(id='plugins/{}'.format(name))
|
if name.lower() in self._plugins:
|
||||||
|
return self._plugins[name.lower()]
|
||||||
|
|
||||||
if not results:
|
results = self.plugins(id='endpoint:plugins/{}'.format(name.lower()),
|
||||||
return Error(message="Plugin not found", status=404)
|
plugin_type=None)
|
||||||
return results[0]
|
if results:
|
||||||
|
return results[0]
|
||||||
|
|
||||||
|
results = self.plugins(id=name,
|
||||||
|
plugin_type=None)
|
||||||
|
if results:
|
||||||
|
return results[0]
|
||||||
|
|
||||||
|
msg = ("Plugin not found: '{}'\n"
|
||||||
|
"Make sure it is ACTIVATED\n"
|
||||||
|
"Valid algorithms: {}").format(name,
|
||||||
|
self._plugins.keys())
|
||||||
|
raise Error(message=msg, status=404)
|
||||||
|
|
||||||
|
def get_plugins(self, name):
|
||||||
|
try:
|
||||||
|
name = name.split(',')
|
||||||
|
except AttributeError:
|
||||||
|
pass # Assume it is a tuple or a list
|
||||||
|
return tuple(self.get_plugin(n) for n in name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def analysis_plugins(self):
|
def analysis_plugins(self):
|
||||||
""" Return only the analysis plugins """
|
""" Return only the analysis plugins that are active"""
|
||||||
return self.plugins(plugin_type='analysisPlugin')
|
return self.plugins(plugin_type='analysisPlugin', is_activated=True)
|
||||||
|
|
||||||
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 """
|
""" Find plugins in this folder and add them to this instance """
|
||||||
@@ -113,93 +133,63 @@ class Senpy(object):
|
|||||||
else:
|
else:
|
||||||
raise AttributeError("Not a folder or does not exist: %s", folder)
|
raise AttributeError("Not a folder or does not exist: %s", folder)
|
||||||
|
|
||||||
def _get_plugins(self, request):
|
# def check_analysis_request(self, analysis):
|
||||||
if not self.analysis_plugins:
|
# '''Check if the analysis request can be fulfilled'''
|
||||||
raise Error(
|
# if not self.plugins():
|
||||||
status=404,
|
# raise Error(
|
||||||
message=("No plugins found."
|
# status=404,
|
||||||
" Please install one."))
|
# message=("No plugins found."
|
||||||
algos = request.parameters.get('algorithm', None)
|
# " Please install one."))
|
||||||
if not algos:
|
# for a in analysis:
|
||||||
if self.default_plugin:
|
# algo = a.algorithm
|
||||||
algos = [self.default_plugin.name, ]
|
# if algo == 'default' and not self.default_plugin:
|
||||||
else:
|
# raise Error(
|
||||||
raise Error(
|
# status=404,
|
||||||
status=404,
|
# message="No default plugin found, and None provided")
|
||||||
message="No default plugin found, and None provided")
|
# else:
|
||||||
|
# self.get_plugin(algo)
|
||||||
|
|
||||||
plugins = list()
|
|
||||||
for algo in algos:
|
|
||||||
algo = algo.lower()
|
|
||||||
if algo not in self._plugins:
|
|
||||||
msg = ("The algorithm '{}' is not valid\n"
|
|
||||||
"Valid algorithms: {}").format(algo,
|
|
||||||
self._plugins.keys())
|
|
||||||
logger.debug(msg)
|
|
||||||
raise Error(
|
|
||||||
status=404,
|
|
||||||
message=msg)
|
|
||||||
plugins.append(self._plugins[algo])
|
|
||||||
return plugins
|
|
||||||
|
|
||||||
def _process_entries(self, entries, req, plugins):
|
def _process(self, req, pending, done=None):
|
||||||
"""
|
"""
|
||||||
Recursively process the entries with the first plugin in the list, and pass the results
|
Recursively process the entries with the first plugin in the list, and pass the results
|
||||||
to the rest of the plugins.
|
to the rest of the plugins.
|
||||||
"""
|
"""
|
||||||
if not plugins:
|
done = done or []
|
||||||
for i in entries:
|
if not pending:
|
||||||
yield i
|
return req
|
||||||
return
|
|
||||||
plugin = plugins[0]
|
analysis = pending[0]
|
||||||
self._activate(plugin) # Make sure the plugin is activated
|
results = analysis.run(req)
|
||||||
specific_params = api.parse_extra_params(req, plugin)
|
results.analysis.append(analysis)
|
||||||
req.analysis.append({'plugin': plugin,
|
done += analysis
|
||||||
'parameters': specific_params})
|
return self._process(results, pending[1:], done)
|
||||||
results = plugin.analyse_entries(entries, specific_params)
|
|
||||||
for i in self._process_entries(results, req, plugins[1:]):
|
|
||||||
yield i
|
|
||||||
|
|
||||||
def install_deps(self):
|
def install_deps(self):
|
||||||
for plugin in self.plugins(is_activated=True):
|
plugins.install_deps(*self.plugins())
|
||||||
plugins.install_deps(plugin)
|
|
||||||
|
|
||||||
def analyse(self, request):
|
def analyse(self, request, analysis=None):
|
||||||
"""
|
"""
|
||||||
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 takes a processed request, provided by the user, as returned
|
||||||
by api.parse_call().
|
by api.parse_call().
|
||||||
"""
|
"""
|
||||||
|
if not self.plugins():
|
||||||
|
raise Error(
|
||||||
|
status=404,
|
||||||
|
message=("No plugins found."
|
||||||
|
" Please install one."))
|
||||||
|
if analysis is None:
|
||||||
|
params = str(request)
|
||||||
|
plugins = self.get_plugins(request.parameters['algorithm'])
|
||||||
|
analysis = api.parse_analysis(request.parameters, plugins)
|
||||||
logger.debug("analysing request: {}".format(request))
|
logger.debug("analysing request: {}".format(request))
|
||||||
try:
|
results = self._process(request, analysis)
|
||||||
entries = request.entries
|
logger.debug("Got analysis result: {}".format(results))
|
||||||
request.entries = []
|
results = self.postprocess(results)
|
||||||
plugins = self._get_plugins(request)
|
logger.debug("Returning post-processed result: {}".format(results))
|
||||||
results = request
|
|
||||||
for i in self._process_entries(entries, results, plugins):
|
|
||||||
results.entries.append(i)
|
|
||||||
self.convert_emotions(results)
|
|
||||||
logger.debug("Returning analysis result: {}".format(results))
|
|
||||||
except (Error, Exception) as ex:
|
|
||||||
if not isinstance(ex, Error):
|
|
||||||
msg = "Error during analysis: {} \n\t{}".format(ex,
|
|
||||||
traceback.format_exc())
|
|
||||||
ex = Error(message=msg, status=500)
|
|
||||||
logger.exception('Error returning analysis result')
|
|
||||||
raise ex
|
|
||||||
results.analysis = [i['plugin'].id for i in results.analysis]
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def _conversion_candidates(self, fromModel, toModel):
|
|
||||||
candidates = self.plugins(plugin_type='emotionConversionPlugin')
|
|
||||||
for candidate in candidates:
|
|
||||||
for pair in candidate.onyx__doesConversion:
|
|
||||||
logging.debug(pair)
|
|
||||||
|
|
||||||
if pair['onyx:conversionFrom'] == fromModel \
|
|
||||||
and pair['onyx:conversionTo'] == toModel:
|
|
||||||
yield candidate
|
|
||||||
|
|
||||||
def convert_emotions(self, resp):
|
def convert_emotions(self, resp):
|
||||||
"""
|
"""
|
||||||
Conversion of all emotions in a response **in place**.
|
Conversion of all emotions in a response **in place**.
|
||||||
@@ -208,11 +198,15 @@ 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]
|
plugins = resp.analysis
|
||||||
params = resp.parameters
|
|
||||||
|
if 'parameters' not in resp:
|
||||||
|
return resp
|
||||||
|
|
||||||
|
params = resp['parameters']
|
||||||
toModel = params.get('emotionModel', None)
|
toModel = params.get('emotionModel', None)
|
||||||
if not toModel:
|
if not toModel:
|
||||||
return
|
return resp
|
||||||
|
|
||||||
logger.debug('Asked for model: {}'.format(toModel))
|
logger.debug('Asked for model: {}'.format(toModel))
|
||||||
output = params.get('conversion', None)
|
output = params.get('conversion', None)
|
||||||
@@ -221,7 +215,8 @@ class Senpy(object):
|
|||||||
try:
|
try:
|
||||||
fromModel = plugin.get('onyx:usesEmotionModel', None)
|
fromModel = plugin.get('onyx:usesEmotionModel', None)
|
||||||
candidates[plugin.id] = next(self._conversion_candidates(fromModel, toModel))
|
candidates[plugin.id] = next(self._conversion_candidates(fromModel, toModel))
|
||||||
logger.debug('Analysis plugin {} uses model: {}'.format(plugin.id, fromModel))
|
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)),
|
||||||
@@ -230,6 +225,7 @@ class Senpy(object):
|
|||||||
e.parameters = params
|
e.parameters = params
|
||||||
raise e
|
raise e
|
||||||
newentries = []
|
newentries = []
|
||||||
|
done = []
|
||||||
for i in resp.entries:
|
for i in resp.entries:
|
||||||
if output == "full":
|
if output == "full":
|
||||||
newemotions = copy.deepcopy(i.emotions)
|
newemotions = copy.deepcopy(i.emotions)
|
||||||
@@ -238,8 +234,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,
|
done.append({'plugin': candidate, 'parameters': params})
|
||||||
'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':
|
||||||
@@ -248,12 +243,83 @@ class Senpy(object):
|
|||||||
i.emotions = newemotions
|
i.emotions = newemotions
|
||||||
newentries.append(i)
|
newentries.append(i)
|
||||||
resp.entries = newentries
|
resp.entries = newentries
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def _conversion_candidates(self, fromModel, toModel):
|
||||||
|
candidates = self.plugins(plugin_type=plugins.EmotionConversion)
|
||||||
|
for candidate in candidates:
|
||||||
|
for pair in candidate.onyx__doesConversion:
|
||||||
|
logging.debug(pair)
|
||||||
|
if candidate.can_convert(fromModel, toModel):
|
||||||
|
yield candidate
|
||||||
|
|
||||||
|
def postprocess(self, response):
|
||||||
|
'''
|
||||||
|
Transform the results from the analysis plugins.
|
||||||
|
It has some pre-defined post-processing like emotion conversion,
|
||||||
|
and it also allows plugins to auto-select themselves.
|
||||||
|
'''
|
||||||
|
|
||||||
|
response = self.convert_emotions(response)
|
||||||
|
|
||||||
|
for plug in self.plugins(plugin_type=plugins.PostProcessing):
|
||||||
|
if plug.check(response, response.analysis):
|
||||||
|
response = plug.process(response)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _get_datasets(self, request):
|
||||||
|
if not self.datasets:
|
||||||
|
raise Error(
|
||||||
|
status=404,
|
||||||
|
message=("No datasets found."
|
||||||
|
" Please verify DatasetManager"))
|
||||||
|
datasets_name = request.parameters.get('dataset', None).split(',')
|
||||||
|
for dataset in datasets_name:
|
||||||
|
if dataset not in self.datasets:
|
||||||
|
logger.debug(("The dataset '{}' is not valid\n"
|
||||||
|
"Valid datasets: {}").format(
|
||||||
|
dataset, self.datasets.keys()))
|
||||||
|
raise Error(
|
||||||
|
status=404,
|
||||||
|
message="The dataset '{}' is not valid".format(dataset))
|
||||||
|
dm = gsitk_compat.DatasetManager()
|
||||||
|
datasets = dm.prepare_datasets(datasets_name)
|
||||||
|
return datasets
|
||||||
|
|
||||||
|
@property
|
||||||
|
def datasets(self):
|
||||||
|
self._dataset_list = {}
|
||||||
|
dm = gsitk_compat.DatasetManager()
|
||||||
|
for item in dm.get_datasets():
|
||||||
|
for key in item:
|
||||||
|
if key in self._dataset_list:
|
||||||
|
continue
|
||||||
|
properties = item[key]
|
||||||
|
properties['@id'] = key
|
||||||
|
self._dataset_list[key] = properties
|
||||||
|
return self._dataset_list
|
||||||
|
|
||||||
|
def evaluate(self, params):
|
||||||
|
logger.debug("evaluating request: {}".format(params))
|
||||||
|
results = AggregatedEvaluation()
|
||||||
|
results.parameters = params
|
||||||
|
datasets = self._get_datasets(results)
|
||||||
|
plugins = []
|
||||||
|
for plugname in params.algorithm:
|
||||||
|
plugins = self.get_plugin(plugname)
|
||||||
|
|
||||||
|
for eval in plugins.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
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_plugin(self):
|
def default_plugin(self):
|
||||||
if not self._default or not self._default.is_activated:
|
if not self._default or not self._default.is_activated:
|
||||||
candidates = self.plugins(plugin_type='analysisPlugin',
|
candidates = self.plugins(
|
||||||
is_activated=True)
|
plugin_type='analysisPlugin', is_activated=True)
|
||||||
if len(candidates) > 0:
|
if len(candidates) > 0:
|
||||||
self._default = candidates[0]
|
self._default = candidates[0]
|
||||||
else:
|
else:
|
||||||
@@ -263,7 +329,7 @@ class Senpy(object):
|
|||||||
|
|
||||||
@default_plugin.setter
|
@default_plugin.setter
|
||||||
def default_plugin(self, value):
|
def default_plugin(self, value):
|
||||||
if isinstance(value, Plugin):
|
if isinstance(value, plugins.Plugin):
|
||||||
if not value.is_activated:
|
if not value.is_activated:
|
||||||
raise AttributeError('The default plugin has to be activated.')
|
raise AttributeError('The default plugin has to be activated.')
|
||||||
self._default = value
|
self._default = value
|
||||||
@@ -271,10 +337,15 @@ class Senpy(object):
|
|||||||
else:
|
else:
|
||||||
self._default = self._plugins[value.lower()]
|
self._default = self._plugins[value.lower()]
|
||||||
|
|
||||||
def activate_all(self, sync=True):
|
def activate_all(self, sync=True, allow_fail=False):
|
||||||
ps = []
|
ps = []
|
||||||
for plug in self._plugins.keys():
|
for plug in self._plugins.keys():
|
||||||
ps.append(self.activate_plugin(plug, sync=sync))
|
try:
|
||||||
|
self.activate_plugin(plug, sync=sync)
|
||||||
|
except Exception as ex:
|
||||||
|
if not allow_fail:
|
||||||
|
raise
|
||||||
|
logger.error('Could not activate {}: {}'.format(plug, ex))
|
||||||
return ps
|
return ps
|
||||||
|
|
||||||
def deactivate_all(self, sync=True):
|
def deactivate_all(self, sync=True):
|
||||||
@@ -299,6 +370,7 @@ class Senpy(object):
|
|||||||
logger.info(msg)
|
logger.info(msg)
|
||||||
success = True
|
success = True
|
||||||
self._set_active(plugin, success)
|
self._set_active(plugin, success)
|
||||||
|
return success
|
||||||
|
|
||||||
def activate_plugin(self, plugin_name, sync=True):
|
def activate_plugin(self, plugin_name, sync=True):
|
||||||
plugin_name = plugin_name.lower()
|
plugin_name = plugin_name.lower()
|
||||||
@@ -309,8 +381,9 @@ class Senpy(object):
|
|||||||
|
|
||||||
logger.info("Activating plugin: {}".format(plugin.name))
|
logger.info("Activating plugin: {}".format(plugin.name))
|
||||||
|
|
||||||
if sync or 'async' in plugin and not plugin.async:
|
if sync or not getattr(plugin, 'async', True) or getattr(
|
||||||
self._activate(plugin)
|
plugin, 'sync', False):
|
||||||
|
return self._activate(plugin)
|
||||||
else:
|
else:
|
||||||
th = Thread(target=partial(self._activate, plugin))
|
th = Thread(target=partial(self._activate, plugin))
|
||||||
th.start()
|
th.start()
|
||||||
@@ -332,7 +405,8 @@ class Senpy(object):
|
|||||||
|
|
||||||
self._set_active(plugin, False)
|
self._set_active(plugin, False)
|
||||||
|
|
||||||
if sync or 'async' in plugin and not plugin.async:
|
if sync or not getattr(plugin, 'async', True) or not getattr(
|
||||||
|
plugin, 'sync', False):
|
||||||
self._deactivate(plugin)
|
self._deactivate(plugin)
|
||||||
else:
|
else:
|
||||||
th = Thread(target=partial(self._deactivate, plugin))
|
th = Thread(target=partial(self._deactivate, plugin))
|
||||||
|
31
senpy/gsitk_compat.py
Normal file
31
senpy/gsitk_compat.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from pkg_resources import parse_version, get_distribution, DistributionNotFound
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
MSG = 'GSITK is not (properly) installed.'
|
||||||
|
IMPORTMSG = '{} Some functions will be unavailable.'.format(MSG)
|
||||||
|
RUNMSG = '{} Install it to use this function.'.format(MSG)
|
||||||
|
|
||||||
|
|
||||||
|
def raise_exception(*args, **kwargs):
|
||||||
|
raise Exception(RUNMSG)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
gsitk_distro = get_distribution("gsitk")
|
||||||
|
GSITK_VERSION = parse_version(gsitk_distro.version)
|
||||||
|
GSITK_AVAILABLE = GSITK_VERSION > parse_version("0.1.9.1") # Earlier versions have a bug
|
||||||
|
except DistributionNotFound:
|
||||||
|
GSITK_AVAILABLE = False
|
||||||
|
GSITK_VERSION = ()
|
||||||
|
|
||||||
|
if GSITK_AVAILABLE:
|
||||||
|
from gsitk.datasets.datasets import DatasetManager
|
||||||
|
from gsitk.evaluation.evaluation import Evaluation as Eval
|
||||||
|
from sklearn.pipeline import Pipeline
|
||||||
|
modules = locals()
|
||||||
|
else:
|
||||||
|
logger.warning(IMPORTMSG)
|
||||||
|
DatasetManager = Eval = Pipeline = raise_exception
|
@@ -85,7 +85,8 @@ class BaseMeta(ABCMeta):
|
|||||||
schema = json.load(f)
|
schema = json.load(f)
|
||||||
|
|
||||||
resolver = jsonschema.RefResolver(schema_path, schema)
|
resolver = jsonschema.RefResolver(schema_path, schema)
|
||||||
attrs['@type'] = "".join((name[0].lower(), name[1:]))
|
if '@type' not in attrs:
|
||||||
|
attrs['@type'] = "".join((name[0].lower(), name[1:]))
|
||||||
attrs['_schema_file'] = schema_file
|
attrs['_schema_file'] = schema_file
|
||||||
attrs['schema'] = schema
|
attrs['schema'] = schema
|
||||||
attrs['_validator'] = jsonschema.Draft4Validator(schema, resolver=resolver)
|
attrs['_validator'] = jsonschema.Draft4Validator(schema, resolver=resolver)
|
||||||
@@ -244,10 +245,10 @@ class CustomDict(MutableMapping, object):
|
|||||||
return key[0] == '_'
|
return key[0] == '_'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.serializable())
|
return json.dumps(self.serializable(), sort_keys=True, indent=4)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return str(self.serializable())
|
return json.dumps(self.serializable(), sort_keys=True, indent=4)
|
||||||
|
|
||||||
|
|
||||||
_Alias = namedtuple('Alias', 'indict')
|
_Alias = namedtuple('Alias', 'indict')
|
||||||
|
116
senpy/models.py
116
senpy/models.py
@@ -51,6 +51,10 @@ 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):
|
||||||
|
return jsonref.dumps(schema)
|
||||||
|
|
||||||
|
|
||||||
def load_context(context):
|
def load_context(context):
|
||||||
logging.debug('Loading context: {}'.format(context))
|
logging.debug('Loading context: {}'.format(context))
|
||||||
if not context:
|
if not context:
|
||||||
@@ -117,11 +121,11 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
|||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
schema_file = DEFINITIONS_FILE
|
# schema_file = DEFINITIONS_FILE
|
||||||
_context = base_context["@context"]
|
_context = base_context["@context"]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
auto_id = kwargs.pop('_auto_id', True)
|
auto_id = kwargs.pop('_auto_id', False)
|
||||||
|
|
||||||
super(BaseModel, self).__init__(*args, **kwargs)
|
super(BaseModel, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
@@ -129,12 +133,12 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
|||||||
self.id
|
self.id
|
||||||
|
|
||||||
if '@type' not in self:
|
if '@type' not in self:
|
||||||
logger.warn('Created an instance of an unknown model')
|
logger.warning('Created an instance of an unknown model')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self):
|
def id(self):
|
||||||
if '@id' not in self:
|
if '@id' not in self:
|
||||||
self['@id'] = ':{}_{}'.format(type(self).__name__, time.time())
|
self['@id'] = '_:{}_{}'.format(type(self).__name__, time.time())
|
||||||
return self['@id']
|
return self['@id']
|
||||||
|
|
||||||
@id.setter
|
@id.setter
|
||||||
@@ -142,7 +146,7 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
|||||||
self['@id'] = value
|
self['@id'] = value
|
||||||
|
|
||||||
def flask(self,
|
def flask(self,
|
||||||
in_headers=True,
|
in_headers=False,
|
||||||
headers=None,
|
headers=None,
|
||||||
outformat='json-ld',
|
outformat='json-ld',
|
||||||
**kwargs):
|
**kwargs):
|
||||||
@@ -172,20 +176,22 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
|||||||
|
|
||||||
def serialize(self, format='json-ld', with_mime=False, **kwargs):
|
def serialize(self, format='json-ld', with_mime=False, **kwargs):
|
||||||
js = self.jsonld(**kwargs)
|
js = self.jsonld(**kwargs)
|
||||||
|
content = json.dumps(js, indent=2, sort_keys=True)
|
||||||
if format == 'json-ld':
|
if format == 'json-ld':
|
||||||
content = json.dumps(js, indent=2, sort_keys=True)
|
|
||||||
mimetype = "application/json"
|
mimetype = "application/json"
|
||||||
elif format in ['turtle', ]:
|
elif format in ['turtle', 'ntriples']:
|
||||||
logger.debug(js)
|
logger.debug(js)
|
||||||
content = json.dumps(js, indent=2, sort_keys=True)
|
base = kwargs.get('prefix')
|
||||||
g = Graph().parse(
|
g = Graph().parse(
|
||||||
data=content,
|
data=content,
|
||||||
format='json-ld',
|
format='json-ld',
|
||||||
base=kwargs.get('prefix'),
|
base=base,
|
||||||
context=self._context)
|
context=[self._context,
|
||||||
|
{'@base': base}])
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'Parsing with prefix: {}'.format(kwargs.get('prefix')))
|
'Parsing with prefix: {}'.format(kwargs.get('prefix')))
|
||||||
content = g.serialize(format='turtle').decode('utf-8')
|
content = g.serialize(format=format,
|
||||||
|
base=base).decode('utf-8')
|
||||||
mimetype = 'text/{}'.format(format)
|
mimetype = 'text/{}'.format(format)
|
||||||
else:
|
else:
|
||||||
raise Error('Unknown outformat: {}'.format(format))
|
raise Error('Unknown outformat: {}'.format(format))
|
||||||
@@ -199,24 +205,23 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
|||||||
context_uri=None,
|
context_uri=None,
|
||||||
prefix=None,
|
prefix=None,
|
||||||
expanded=False):
|
expanded=False):
|
||||||
ser = self.serializable()
|
|
||||||
|
|
||||||
result = jsonld.compact(
|
result = self.serializable()
|
||||||
ser,
|
|
||||||
self._context,
|
|
||||||
options={
|
|
||||||
'base': prefix,
|
|
||||||
'expandContext': self._context,
|
|
||||||
'senpy': prefix
|
|
||||||
})
|
|
||||||
if context_uri:
|
|
||||||
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})[0]
|
||||||
if not with_context:
|
if not with_context:
|
||||||
del result['@context']
|
try:
|
||||||
|
del result['@context']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
elif context_uri:
|
||||||
|
result['@context'] = context_uri
|
||||||
|
else:
|
||||||
|
result['@context'] = self._context
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def validate(self, obj=None):
|
def validate(self, obj=None):
|
||||||
@@ -319,7 +324,9 @@ def _add_class_from_schema(*args, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
for i in [
|
for i in [
|
||||||
'analysis',
|
'aggregatedEvaluation',
|
||||||
|
'dataset',
|
||||||
|
'datasets',
|
||||||
'emotion',
|
'emotion',
|
||||||
'emotionConversion',
|
'emotionConversion',
|
||||||
'emotionConversionPlugin',
|
'emotionConversionPlugin',
|
||||||
@@ -327,12 +334,69 @@ for i in [
|
|||||||
'emotionModel',
|
'emotionModel',
|
||||||
'emotionPlugin',
|
'emotionPlugin',
|
||||||
'emotionSet',
|
'emotionSet',
|
||||||
|
'evaluation',
|
||||||
|
'entity',
|
||||||
'help',
|
'help',
|
||||||
'plugin',
|
'metric',
|
||||||
|
'parameter',
|
||||||
'plugins',
|
'plugins',
|
||||||
'response',
|
'response',
|
||||||
'results',
|
'results',
|
||||||
'sentimentPlugin',
|
'sentimentPlugin',
|
||||||
'suggestion',
|
'suggestion',
|
||||||
|
'topic',
|
||||||
|
|
||||||
]:
|
]:
|
||||||
_add_class_from_schema(i)
|
_add_class_from_schema(i)
|
||||||
|
|
||||||
|
|
||||||
|
class Analysis(BaseModel):
|
||||||
|
schema = 'analysis'
|
||||||
|
|
||||||
|
parameters = alias('prov:used')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def params(self):
|
||||||
|
outdict = {}
|
||||||
|
outdict['algorithm'] = self.algorithm
|
||||||
|
for param in self.parameters:
|
||||||
|
outdict[param['name']] = param['value']
|
||||||
|
return outdict
|
||||||
|
|
||||||
|
@params.setter
|
||||||
|
def params(self, value):
|
||||||
|
for k, v in value.items():
|
||||||
|
for param in self.parameters:
|
||||||
|
if param.name == k:
|
||||||
|
param.value = v
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.parameters.append(Parameter(name=k, value=v))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def algorithm(self):
|
||||||
|
return self['prov:wasAssociatedWith']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugin(self):
|
||||||
|
return self._plugin
|
||||||
|
|
||||||
|
@plugin.setter
|
||||||
|
def plugin(self, value):
|
||||||
|
self._plugin = value
|
||||||
|
self['prov:wasAssociatedWith'] = value.id
|
||||||
|
|
||||||
|
def run(self, request):
|
||||||
|
return self.plugin.process(request, self.params)
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin(BaseModel):
|
||||||
|
schema = 'plugin'
|
||||||
|
|
||||||
|
def activity(self, parameters):
|
||||||
|
'''Generate a prov:Activity from this plugin and the '''
|
||||||
|
a = Analysis()
|
||||||
|
a.plugin = self
|
||||||
|
a.params = parameters
|
||||||
|
return a
|
||||||
|
|
||||||
|
@@ -1,15 +1,14 @@
|
|||||||
from future import standard_library
|
from future import standard_library
|
||||||
standard_library.install_aliases()
|
standard_library.install_aliases()
|
||||||
|
|
||||||
|
|
||||||
from future.utils import with_metaclass
|
from future.utils import with_metaclass
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import pickle
|
import pickle
|
||||||
import logging
|
import logging
|
||||||
import copy
|
|
||||||
import pprint
|
import pprint
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
@@ -18,9 +17,12 @@ import subprocess
|
|||||||
import importlib
|
import importlib
|
||||||
import yaml
|
import yaml
|
||||||
import threading
|
import threading
|
||||||
|
from nltk import download
|
||||||
|
|
||||||
from .. import models, utils
|
from .. import models, utils
|
||||||
from .. import api
|
from .. import api
|
||||||
|
from .. import gsitk_compat
|
||||||
|
from .. import testing
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -38,19 +40,22 @@ class PluginMeta(models.BaseMeta):
|
|||||||
attrs['name'] = alias
|
attrs['name'] = alias
|
||||||
if 'description' not in attrs:
|
if 'description' not in attrs:
|
||||||
doc = attrs.get('__doc__', None)
|
doc = attrs.get('__doc__', None)
|
||||||
if not doc:
|
if doc:
|
||||||
raise Exception(('Please, add a description or '
|
attrs['description'] = doc
|
||||||
'documentation to class {}').format(name))
|
else:
|
||||||
attrs['description'] = doc
|
logger.warning(
|
||||||
attrs['name'] = alias
|
('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)
|
cls = super(PluginMeta, mcs).__new__(mcs, name, bases, attrs)
|
||||||
|
|
||||||
if alias in mcs._classes:
|
if alias in mcs._classes:
|
||||||
if os.environ.get('SENPY_TESTING', ""):
|
if os.environ.get('SENPY_TESTING', ""):
|
||||||
raise Exception(('The type of plugin {} already exists. '
|
raise Exception(
|
||||||
'Please, choose a different name').format(name))
|
('The type of plugin {} already exists. '
|
||||||
|
'Please, choose a different name').format(name))
|
||||||
else:
|
else:
|
||||||
logger.warn('Overloading plugin class: {}'.format(alias))
|
logger.warning('Overloading plugin class: {}'.format(alias))
|
||||||
mcs._classes[alias] = cls
|
mcs._classes[alias] = cls
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
@@ -82,10 +87,32 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
|||||||
if info:
|
if info:
|
||||||
self.update(info)
|
self.update(info)
|
||||||
self.validate()
|
self.validate()
|
||||||
self.id = 'plugins/{}_{}'.format(self['name'], self['version'])
|
self.id = 'endpoint:plugins/{}_{}'.format(self['name'],
|
||||||
|
self['version'])
|
||||||
self.is_activated = False
|
self.is_activated = False
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
self.data_folder = data_folder or os.getcwd()
|
self._directory = os.path.abspath(
|
||||||
|
os.path.dirname(inspect.getfile(self.__class__)))
|
||||||
|
|
||||||
|
data_folder = data_folder or os.getcwd()
|
||||||
|
subdir = os.path.join(data_folder, self.name)
|
||||||
|
|
||||||
|
self._data_paths = [
|
||||||
|
data_folder,
|
||||||
|
subdir,
|
||||||
|
self._directory,
|
||||||
|
os.path.join(self._directory, 'data'),
|
||||||
|
]
|
||||||
|
|
||||||
|
if os.path.exists(subdir):
|
||||||
|
data_folder = subdir
|
||||||
|
self.data_folder = data_folder
|
||||||
|
|
||||||
|
self._log = logging.getLogger('{}.{}'.format(__name__, self.name))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def log(self):
|
||||||
|
return self._log
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
missing = []
|
missing = []
|
||||||
@@ -93,7 +120,8 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
|||||||
if x not in self:
|
if x not in self:
|
||||||
missing.append(x)
|
missing.append(x)
|
||||||
if missing:
|
if missing:
|
||||||
raise models.Error('Missing configuration parameters: {}'.format(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__))
|
||||||
@@ -104,45 +132,108 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
|||||||
def deactivate(self):
|
def deactivate(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def process(self, request, parameters, **kwargs):
|
||||||
|
"""
|
||||||
|
An implemented plugin should override this method.
|
||||||
|
Here, we assume that a process_entries method exists."""
|
||||||
|
newentries = list(
|
||||||
|
self.process_entries(request.entries, parameters))
|
||||||
|
request.entries = newentries
|
||||||
|
return request
|
||||||
|
|
||||||
|
def process_entries(self, entries, parameters):
|
||||||
|
for entry in entries:
|
||||||
|
self.log.debug('Processing entry with plugin {}: {}'.format(
|
||||||
|
self, entry))
|
||||||
|
results = self.process_entry(entry, parameters)
|
||||||
|
if inspect.isgenerator(results):
|
||||||
|
for result in results:
|
||||||
|
yield result
|
||||||
|
else:
|
||||||
|
yield results
|
||||||
|
|
||||||
|
def process_entry(self, entry, parameters):
|
||||||
|
"""
|
||||||
|
This base method is here to adapt plugins which only
|
||||||
|
implement the *process* function.
|
||||||
|
Note that this method may yield an annotated entry or a list of
|
||||||
|
entries (e.g. in a tokenizer)
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
'You need to implement process, process_entries or process_entry in your plugin'
|
||||||
|
)
|
||||||
|
|
||||||
def test(self, test_cases=None):
|
def test(self, test_cases=None):
|
||||||
if not test_cases:
|
if not test_cases:
|
||||||
if not hasattr(self, 'test_cases'):
|
if not hasattr(self, 'test_cases'):
|
||||||
raise AttributeError(('Plugin {} [{}] does not have any defined '
|
raise AttributeError(
|
||||||
'test cases').format(self.id,
|
('Plugin {} [{}] does not have any defined '
|
||||||
inspect.getfile(self.__class__)))
|
'test cases').format(self.id,
|
||||||
|
inspect.getfile(self.__class__)))
|
||||||
test_cases = self.test_cases
|
test_cases = self.test_cases
|
||||||
for case in test_cases:
|
for case in test_cases:
|
||||||
try:
|
try:
|
||||||
self.test_case(case)
|
self.test_case(case)
|
||||||
logger.debug('Test case passed:\n{}'.format(pprint.pformat(case)))
|
self.log.debug('Test case passed:\n{}'.format(
|
||||||
|
pprint.pformat(case)))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warn('Test case failed:\n{}'.format(pprint.pformat(case)))
|
self.log.warning('Test case failed:\n{}'.format(
|
||||||
|
pprint.pformat(case)))
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def test_case(self, case):
|
def test_case(self, case, mock=testing.MOCK_REQUESTS):
|
||||||
|
if 'entry' not in case and 'input' in case:
|
||||||
|
entry = models.Entry(_auto_id=False)
|
||||||
|
entry.nif__isString = case['input']
|
||||||
|
case['entry'] = entry
|
||||||
entry = models.Entry(case['entry'])
|
entry = models.Entry(case['entry'])
|
||||||
given_parameters = case.get('params', case.get('parameters', {}))
|
given_parameters = case.get('params', case.get('parameters', {}))
|
||||||
expected = case.get('expected', None)
|
expected = case.get('expected', None)
|
||||||
should_fail = case.get('should_fail', False)
|
should_fail = case.get('should_fail', False)
|
||||||
|
responses = case.get('responses', [])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
params = api.parse_params(given_parameters, self.extra_params)
|
request = models.Response()
|
||||||
res = list(self.analyse_entries([entry, ], params))
|
parameters = api.parse_params(given_parameters,
|
||||||
|
self.extra_params)
|
||||||
|
request.entries = [
|
||||||
|
entry,
|
||||||
|
]
|
||||||
|
|
||||||
|
method = partial(self.process, request, parameters)
|
||||||
|
|
||||||
|
if mock:
|
||||||
|
res = method()
|
||||||
|
else:
|
||||||
|
with testing.patch_all_requests(responses):
|
||||||
|
res = method()
|
||||||
|
|
||||||
if not isinstance(expected, list):
|
if not isinstance(expected, list):
|
||||||
expected = [expected]
|
expected = [expected]
|
||||||
utils.check_template(res, expected)
|
utils.check_template(res.entries, expected)
|
||||||
for r in res:
|
res.validate()
|
||||||
r.validate()
|
|
||||||
except models.Error:
|
except models.Error:
|
||||||
if should_fail:
|
if should_fail:
|
||||||
return
|
return
|
||||||
raise
|
raise
|
||||||
assert not should_fail
|
assert not should_fail
|
||||||
|
|
||||||
def open(self, fpath, *args, **kwargs):
|
def find_file(self, fname):
|
||||||
if not os.path.isabs(fpath):
|
for p in self._data_paths:
|
||||||
fpath = os.path.join(self.data_folder, fpath)
|
alternative = os.path.join(p, fname)
|
||||||
return open(fpath, *args, **kwargs)
|
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):
|
def serve(self, debug=True, **kwargs):
|
||||||
utils.easy(plugin_list=[self, ], plugin_folder=None, debug=debug, **kwargs)
|
utils.easy(plugin_list=[self, ], plugin_folder=None, debug=debug, **kwargs)
|
||||||
@@ -157,40 +248,26 @@ class Analysis(Plugin):
|
|||||||
A subclass of Plugin that analyses text and provides an annotation.
|
A subclass of Plugin that analyses text and provides an annotation.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def analyse(self, *args, **kwargs):
|
def analyse(self, request, parameters):
|
||||||
raise NotImplementedError(
|
return super(Analysis, self).process(request, parameters)
|
||||||
'Your plugin should implement either analyse or analyse_entry')
|
|
||||||
|
|
||||||
def analyse_entry(self, entry, parameters):
|
|
||||||
""" An implemented plugin should override this method.
|
|
||||||
This base method is here to adapt old style plugins which only
|
|
||||||
implement the *analyse* function.
|
|
||||||
Note that this method may yield an annotated entry or a list of
|
|
||||||
entries (e.g. in a tokenizer)
|
|
||||||
"""
|
|
||||||
text = entry['nif:isString']
|
|
||||||
params = copy.copy(parameters)
|
|
||||||
params['input'] = text
|
|
||||||
results = self.analyse(**params)
|
|
||||||
for i in results.entries:
|
|
||||||
yield i
|
|
||||||
|
|
||||||
def analyse_entries(self, entries, parameters):
|
def analyse_entries(self, entries, parameters):
|
||||||
for entry in entries:
|
for i in super(Analysis, self).process_entries(entries, parameters):
|
||||||
logger.debug('Analysing entry with plugin {}: {}'.format(self, entry))
|
yield i
|
||||||
results = self.analyse_entry(entry, parameters)
|
|
||||||
if inspect.isgenerator(results):
|
|
||||||
for result in results:
|
|
||||||
yield result
|
|
||||||
else:
|
|
||||||
yield results
|
|
||||||
|
|
||||||
def test_case(self, case):
|
def process(self, request, parameters, **kwargs):
|
||||||
if 'entry' not in case and 'input' in case:
|
return self.analyse(request, parameters)
|
||||||
entry = models.Entry(_auto_id=False)
|
|
||||||
entry.nif__isString = case['input']
|
def process_entries(self, entries, parameters):
|
||||||
case['entry'] = entry
|
for i in self.analyse_entries(entries, parameters):
|
||||||
super(Analysis, self).test_case(case)
|
yield i
|
||||||
|
|
||||||
|
def process_entry(self, entry, parameters, **kwargs):
|
||||||
|
if hasattr(self, 'analyse_entry'):
|
||||||
|
for i in self.analyse_entry(entry, parameters):
|
||||||
|
yield i
|
||||||
|
else:
|
||||||
|
super(Analysis, self).process_entry(entry, parameters, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
AnalysisPlugin = Analysis
|
AnalysisPlugin = Analysis
|
||||||
@@ -201,7 +278,20 @@ class Conversion(Plugin):
|
|||||||
A subclass of Plugins that convert between different annotation models.
|
A subclass of Plugins that convert between different annotation models.
|
||||||
e.g. a conversion of emotion models, or normalization of sentiment values.
|
e.g. a conversion of emotion models, or normalization of sentiment values.
|
||||||
'''
|
'''
|
||||||
pass
|
|
||||||
|
def process(self, response, parameters, plugins=None, **kwargs):
|
||||||
|
plugins = plugins or []
|
||||||
|
newentries = []
|
||||||
|
for entry in response.entries:
|
||||||
|
newentries.append(
|
||||||
|
self.convert_entry(entry, parameters, plugins))
|
||||||
|
response.entries = newentries
|
||||||
|
return response
|
||||||
|
|
||||||
|
def convert_entry(self, entry, parameters, conversions_applied):
|
||||||
|
raise NotImplementedError(
|
||||||
|
'You should implement a way to convert each entry, or a custom process method'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
ConversionPlugin = Conversion
|
ConversionPlugin = Conversion
|
||||||
@@ -238,12 +328,28 @@ class EmotionConversion(Conversion):
|
|||||||
'''
|
'''
|
||||||
A subclass of Conversion that converts emotion annotations using different models
|
A subclass of Conversion that converts emotion annotations using different models
|
||||||
'''
|
'''
|
||||||
pass
|
|
||||||
|
def can_convert(self, fromModel, toModel):
|
||||||
|
'''
|
||||||
|
Whether this plugin can convert from fromModel to toModel.
|
||||||
|
If fromModel is None, it is interpreted as "any Model"
|
||||||
|
'''
|
||||||
|
for pair in self.onyx__doesConversion:
|
||||||
|
if (pair['onyx:conversionTo'] == toModel) and \
|
||||||
|
((fromModel is None) or (pair['onyx:conversionFrom'] == fromModel)):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
EmotionConversionPlugin = EmotionConversion
|
EmotionConversionPlugin = EmotionConversion
|
||||||
|
|
||||||
|
|
||||||
|
class PostProcessing(Plugin):
|
||||||
|
def check(self, request, plugins):
|
||||||
|
'''Should this plugin be run for this request?'''
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Box(AnalysisPlugin):
|
class Box(AnalysisPlugin):
|
||||||
'''
|
'''
|
||||||
Black box plugins delegate analysis to a function.
|
Black box plugins delegate analysis to a function.
|
||||||
@@ -251,7 +357,7 @@ class Box(AnalysisPlugin):
|
|||||||
|
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
entry --> input() --> predict() --> output() --> entry'
|
entry --> input() --> predict_one() --> output() --> entry'
|
||||||
|
|
||||||
|
|
||||||
In other words: their ``input`` method convers a query (entry and a set of parameters) into
|
In other words: their ``input`` method convers a query (entry and a set of parameters) into
|
||||||
@@ -267,15 +373,34 @@ class Box(AnalysisPlugin):
|
|||||||
'''Transforms the results of the black box into an entry'''
|
'''Transforms the results of the black box into an entry'''
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def predict(self, input):
|
def predict_one(self, input):
|
||||||
raise NotImplementedError('You should define the behavior of this plugin')
|
raise NotImplementedError(
|
||||||
|
'You should define the behavior of this plugin')
|
||||||
|
|
||||||
def analyse_entries(self, entries, params):
|
def process_entries(self, entries, params):
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
input = self.input(entry=entry, params=params)
|
input = self.input(entry=entry, params=params)
|
||||||
results = self.predict(input=input)
|
results = self.predict_one(input=input)
|
||||||
yield self.output(output=results, entry=entry, params=params)
|
yield self.output(output=results, entry=entry, params=params)
|
||||||
|
|
||||||
|
def fit(self, X=None, y=None):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def transform(self, X):
|
||||||
|
return [self.predict_one(x) for x in X]
|
||||||
|
|
||||||
|
def predict(self, X):
|
||||||
|
return self.transform(X)
|
||||||
|
|
||||||
|
def fit_transform(self, X, y):
|
||||||
|
self.fit(X, y)
|
||||||
|
return self.transform(X)
|
||||||
|
|
||||||
|
def as_pipe(self):
|
||||||
|
pipe = gsitk_compat.Pipeline([('plugin', self)])
|
||||||
|
pipe.name = self.name
|
||||||
|
return pipe
|
||||||
|
|
||||||
|
|
||||||
class TextBox(Box):
|
class TextBox(Box):
|
||||||
'''A black box plugin that takes only text as input'''
|
'''A black box plugin that takes only text as input'''
|
||||||
@@ -321,7 +446,6 @@ class EmotionBox(TextBox, EmotionPlugin):
|
|||||||
|
|
||||||
|
|
||||||
class MappingMixin(object):
|
class MappingMixin(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mappings(self):
|
def mappings(self):
|
||||||
return self._mappings
|
return self._mappings
|
||||||
@@ -331,11 +455,10 @@ class MappingMixin(object):
|
|||||||
self._mappings = value
|
self._mappings = value
|
||||||
|
|
||||||
def output(self, output, entry, params):
|
def output(self, output, entry, params):
|
||||||
output = self.mappings.get(output,
|
output = self.mappings.get(output, self.mappings.get(
|
||||||
self.mappings.get('default', output))
|
'default', output))
|
||||||
return super(MappingMixin, self).output(output=output,
|
return super(MappingMixin, self).output(
|
||||||
entry=entry,
|
output=output, entry=entry, params=params)
|
||||||
params=params)
|
|
||||||
|
|
||||||
|
|
||||||
class ShelfMixin(object):
|
class ShelfMixin(object):
|
||||||
@@ -348,7 +471,8 @@ class ShelfMixin(object):
|
|||||||
with self.open(self.shelf_file, 'rb') as p:
|
with self.open(self.shelf_file, 'rb') as p:
|
||||||
self._sh = pickle.load(p)
|
self._sh = pickle.load(p)
|
||||||
except (IndexError, EOFError, pickle.UnpicklingError):
|
except (IndexError, EOFError, pickle.UnpicklingError):
|
||||||
logger.warning('{} has a corrupted shelf file!'.format(self.id))
|
self.log.warning('Corrupted shelf file: {}'.format(
|
||||||
|
self.shelf_file))
|
||||||
if not self.get('force_shelf', False):
|
if not self.get('force_shelf', False):
|
||||||
raise
|
raise
|
||||||
return self._sh
|
return self._sh
|
||||||
@@ -375,32 +499,30 @@ class ShelfMixin(object):
|
|||||||
self._shelf_file = value
|
self._shelf_file = value
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
logger.debug('saving pickle')
|
self.log.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 self.open(self.shelf_file, 'wb') as f:
|
||||||
pickle.dump(self._sh, f)
|
pickle.dump(self._sh, f)
|
||||||
|
|
||||||
|
|
||||||
def pfilter(plugins, **kwargs):
|
def pfilter(plugins, plugin_type=Analysis, **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', Plugin)
|
|
||||||
logger.debug('#' * 100)
|
logger.debug('#' * 100)
|
||||||
logger.debug('ptype {}'.format(ptype))
|
logger.debug('plugin_type {}'.format(plugin_type))
|
||||||
if ptype:
|
if plugin_type:
|
||||||
if isinstance(ptype, PluginMeta):
|
if isinstance(plugin_type, PluginMeta):
|
||||||
ptype = ptype.__name__
|
plugin_type = plugin_type.__name__
|
||||||
try:
|
try:
|
||||||
ptype = ptype[0].upper() + ptype[1:]
|
plugin_type = plugin_type[0].upper() + plugin_type[1:]
|
||||||
pclass = globals()[ptype]
|
pclass = globals()[plugin_type]
|
||||||
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(ptype))
|
raise models.Error('{} is not a valid type'.format(plugin_type))
|
||||||
else:
|
else:
|
||||||
candidates = plugins
|
candidates = plugins
|
||||||
|
|
||||||
@@ -408,8 +530,7 @@ def pfilter(plugins, **kwargs):
|
|||||||
|
|
||||||
def matches(plug):
|
def matches(plug):
|
||||||
res = all(getattr(plug, k, None) == v for (k, v) in kwargs.items())
|
res = all(getattr(plug, k, None) == v for (k, v) in kwargs.items())
|
||||||
logger.debug(
|
logger.debug("matching {} with {}: {}".format(plug.name, kwargs, res))
|
||||||
"matching {} with {}: {}".format(plug.name, kwargs, res))
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
if kwargs:
|
if kwargs:
|
||||||
@@ -435,21 +556,25 @@ def _log_subprocess_output(process):
|
|||||||
|
|
||||||
def install_deps(*plugins):
|
def install_deps(*plugins):
|
||||||
installed = False
|
installed = False
|
||||||
|
nltk_resources = set()
|
||||||
for info in plugins:
|
for info in plugins:
|
||||||
requirements = info.get('requirements', [])
|
requirements = info.get('requirements', [])
|
||||||
if requirements:
|
if requirements:
|
||||||
pip_args = [sys.executable, '-m', 'pip', 'install', '--use-wheel']
|
pip_args = [sys.executable, '-m', 'pip', 'install']
|
||||||
for req in requirements:
|
for req in requirements:
|
||||||
pip_args.append(req)
|
pip_args.append(req)
|
||||||
logger.info('Installing requirements: ' + str(requirements))
|
logger.info('Installing requirements: ' + str(requirements))
|
||||||
process = subprocess.Popen(pip_args,
|
process = subprocess.Popen(
|
||||||
stdout=subprocess.PIPE,
|
pip_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
stderr=subprocess.PIPE)
|
|
||||||
_log_subprocess_output(process)
|
_log_subprocess_output(process)
|
||||||
exitcode = process.wait()
|
exitcode = process.wait()
|
||||||
installed = True
|
installed = True
|
||||||
if exitcode != 0:
|
if exitcode != 0:
|
||||||
raise models.Error("Dependencies not properly installed")
|
raise models.Error(
|
||||||
|
"Dependencies not properly installed: {}".format(pip_args))
|
||||||
|
nltk_resources |= set(info.get('nltk_resources', []))
|
||||||
|
|
||||||
|
installed |= download(list(nltk_resources))
|
||||||
return installed
|
return installed
|
||||||
|
|
||||||
|
|
||||||
@@ -467,7 +592,7 @@ def find_plugins(folders):
|
|||||||
yield fpath
|
yield fpath
|
||||||
|
|
||||||
|
|
||||||
def from_path(fpath, **kwargs):
|
def from_path(fpath, install_on_fail=False, **kwargs):
|
||||||
logger.debug("Loading plugin from {}".format(fpath))
|
logger.debug("Loading plugin from {}".format(fpath))
|
||||||
if fpath.endswith('.py'):
|
if fpath.endswith('.py'):
|
||||||
# We asume root is the dir of the file, and module is the name of the file
|
# We asume root is the dir of the file, and module is the name of the file
|
||||||
@@ -477,7 +602,7 @@ def from_path(fpath, **kwargs):
|
|||||||
yield instance
|
yield instance
|
||||||
else:
|
else:
|
||||||
info = parse_plugin_info(fpath)
|
info = parse_plugin_info(fpath)
|
||||||
yield from_info(info, **kwargs)
|
yield from_info(info, install_on_fail=install_on_fail, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def from_folder(folders, loader=from_path, **kwargs):
|
def from_folder(folders, loader=from_path, **kwargs):
|
||||||
@@ -488,15 +613,20 @@ def from_folder(folders, loader=from_path, **kwargs):
|
|||||||
return plugins
|
return plugins
|
||||||
|
|
||||||
|
|
||||||
def from_info(info, root=None, **kwargs):
|
def from_info(info, root=None, install_on_fail=True, **kwargs):
|
||||||
if any(x not in info for x in ('module',)):
|
if any(x not in info for x in ('module', )):
|
||||||
raise ValueError('Plugin info is not valid: {}'.format(info))
|
raise ValueError('Plugin info is not valid: {}'.format(info))
|
||||||
module = info["module"]
|
module = info["module"]
|
||||||
|
|
||||||
if not root and '_path' in info:
|
if not root and '_path' in info:
|
||||||
root = os.path.dirname(info['_path'])
|
root = os.path.dirname(info['_path'])
|
||||||
|
|
||||||
return one_from_module(module, root=root, info=info, **kwargs)
|
fun = partial(one_from_module, module, root=root, info=info, **kwargs)
|
||||||
|
try:
|
||||||
|
return fun()
|
||||||
|
except (ImportError, LookupError):
|
||||||
|
install_deps(info)
|
||||||
|
return fun()
|
||||||
|
|
||||||
|
|
||||||
def parse_plugin_info(fpath):
|
def parse_plugin_info(fpath):
|
||||||
@@ -521,7 +651,8 @@ def one_from_module(module, root, info, **kwargs):
|
|||||||
if '@type' in info:
|
if '@type' in info:
|
||||||
cls = PluginMeta.from_type(info['@type'])
|
cls = PluginMeta.from_type(info['@type'])
|
||||||
return cls(info=info, **kwargs)
|
return cls(info=info, **kwargs)
|
||||||
instance = next(from_module(module=module, root=root, info=info, **kwargs), None)
|
instance = next(
|
||||||
|
from_module(module=module, root=root, info=info, **kwargs), None)
|
||||||
if not instance:
|
if not instance:
|
||||||
raise Exception("No valid plugin for: {}".format(module))
|
raise Exception("No valid plugin for: {}".format(module))
|
||||||
return instance
|
return instance
|
||||||
@@ -543,15 +674,10 @@ def _instances_in_module(module):
|
|||||||
yield obj
|
yield obj
|
||||||
|
|
||||||
|
|
||||||
def _from_module_name(module, root, info=None, install=True, **kwargs):
|
def _from_module_name(module, root, info=None, **kwargs):
|
||||||
try:
|
module = load_module(module, root)
|
||||||
module = load_module(module, root)
|
for plugin in _from_loaded_module(
|
||||||
except ImportError:
|
module=module, root=root, info=info, **kwargs):
|
||||||
if not install or not info:
|
|
||||||
raise
|
|
||||||
install_deps(info)
|
|
||||||
module = load_module(module, root)
|
|
||||||
for plugin in _from_loaded_module(module=module, root=root, info=info, **kwargs):
|
|
||||||
yield plugin
|
yield plugin
|
||||||
|
|
||||||
|
|
||||||
@@ -560,3 +686,48 @@ def _from_loaded_module(module, info=None, **kwargs):
|
|||||||
yield cls(info=info, **kwargs)
|
yield cls(info=info, **kwargs)
|
||||||
for instance in _instances_in_module(module):
|
for instance in _instances_in_module(module):
|
||||||
yield instance
|
yield instance
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate(plugins, datasets, **kwargs):
|
||||||
|
ev = gsitk_compat.Eval(
|
||||||
|
tuples=None,
|
||||||
|
datasets=datasets,
|
||||||
|
pipelines=[plugin.as_pipe() for plugin in plugins])
|
||||||
|
ev.evaluate()
|
||||||
|
results = ev.results
|
||||||
|
evaluations = evaluations_to_JSONLD(results, **kwargs)
|
||||||
|
return evaluations
|
||||||
|
|
||||||
|
|
||||||
|
def evaluations_to_JSONLD(results, flatten=False):
|
||||||
|
'''
|
||||||
|
Map the evaluation results to a JSONLD scheme
|
||||||
|
'''
|
||||||
|
|
||||||
|
evaluations = list()
|
||||||
|
metric_names = ['accuracy', 'precision_macro', 'recall_macro',
|
||||||
|
'f1_macro', 'f1_weighted', 'f1_micro', 'f1_macro']
|
||||||
|
|
||||||
|
for index, row in results.iterrows():
|
||||||
|
evaluation = models.Evaluation()
|
||||||
|
if row.get('CV', True):
|
||||||
|
evaluation['@type'] = ['StaticCV', 'Evaluation']
|
||||||
|
evaluation.evaluatesOn = row['Dataset']
|
||||||
|
evaluation.evaluates = row['Model']
|
||||||
|
i = 0
|
||||||
|
if flatten:
|
||||||
|
metric = models.Metric()
|
||||||
|
for name in metric_names:
|
||||||
|
metric[name] = row[name]
|
||||||
|
evaluation.metrics.append(metric)
|
||||||
|
else:
|
||||||
|
# We should probably discontinue this representation
|
||||||
|
for name in metric_names:
|
||||||
|
metric = models.Metric()
|
||||||
|
metric['@id'] = 'Metric' + str(i)
|
||||||
|
metric['@type'] = name.capitalize()
|
||||||
|
metric.value = row[name]
|
||||||
|
evaluation.metrics.append(metric)
|
||||||
|
i += 1
|
||||||
|
evaluations.append(evaluation)
|
||||||
|
return evaluations
|
||||||
|
@@ -1,19 +0,0 @@
|
|||||||
---
|
|
||||||
name: split
|
|
||||||
module: senpy.plugins.misc.split
|
|
||||||
description: A sample plugin that chunks input text
|
|
||||||
author: "@militarpancho"
|
|
||||||
version: '0.2'
|
|
||||||
url: "https://github.com/gsi-upm/senpy"
|
|
||||||
requirements:
|
|
||||||
- nltk
|
|
||||||
extra_params:
|
|
||||||
delimiter:
|
|
||||||
aliases:
|
|
||||||
- type
|
|
||||||
- t
|
|
||||||
required: false
|
|
||||||
default: sentence
|
|
||||||
options:
|
|
||||||
- sentence
|
|
||||||
- paragraph
|
|
@@ -5,13 +5,27 @@ from nltk.tokenize.simple import LineTokenizer
|
|||||||
import nltk
|
import nltk
|
||||||
|
|
||||||
|
|
||||||
class SplitPlugin(AnalysisPlugin):
|
class Split(AnalysisPlugin):
|
||||||
'''description: A sample plugin that chunks input text'''
|
'''description: A sample plugin that chunks input text'''
|
||||||
|
|
||||||
|
author = ["@militarpancho", '@balkian']
|
||||||
|
version = '0.3'
|
||||||
|
url = "https://github.com/gsi-upm/senpy"
|
||||||
|
|
||||||
|
extra_params = {
|
||||||
|
'delimiter': {
|
||||||
|
'aliases': ['type', 't'],
|
||||||
|
'required': False,
|
||||||
|
'default': 'sentence',
|
||||||
|
'options': ['sentence', 'paragraph']
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
def activate(self):
|
def activate(self):
|
||||||
nltk.download('punkt')
|
nltk.download('punkt')
|
||||||
|
|
||||||
def analyse_entry(self, entry, params):
|
def analyse_entry(self, entry, params):
|
||||||
|
yield entry
|
||||||
chunker_type = params["delimiter"]
|
chunker_type = params["delimiter"]
|
||||||
original_text = entry['nif:isString']
|
original_text = entry['nif:isString']
|
||||||
if chunker_type == "sentence":
|
if chunker_type == "sentence":
|
||||||
@@ -19,12 +33,15 @@ class SplitPlugin(AnalysisPlugin):
|
|||||||
if chunker_type == "paragraph":
|
if chunker_type == "paragraph":
|
||||||
tokenizer = LineTokenizer()
|
tokenizer = LineTokenizer()
|
||||||
chars = list(tokenizer.span_tokenize(original_text))
|
chars = list(tokenizer.span_tokenize(original_text))
|
||||||
for i, chunk in enumerate(tokenizer.tokenize(original_text)):
|
if len(chars) == 1:
|
||||||
print(chunk)
|
# This sentence was already split
|
||||||
|
return
|
||||||
|
for i, chunk in enumerate(chars):
|
||||||
|
start, end = chunk
|
||||||
e = Entry()
|
e = Entry()
|
||||||
e['nif:isString'] = chunk
|
e['nif:isString'] = original_text[start:end]
|
||||||
if entry.id:
|
if entry.id:
|
||||||
e.id = entry.id + "#char={},{}".format(chars[i][0], chars[i][1])
|
e.id = entry.id + "#char={},{}".format(start, end)
|
||||||
yield e
|
yield e
|
||||||
|
|
||||||
test_cases = [
|
test_cases = [
|
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: Ekman2FSRE
|
name: Ekman2FSRE
|
||||||
module: senpy.plugins.conversion.emotion.centroids
|
module: senpy.plugins.postprocessing.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.2
|
||||||
# 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
|
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: Ekman2PAD
|
name: Ekman2PAD
|
||||||
module: senpy.plugins.conversion.emotion.centroids
|
module: senpy.plugins.postprocessing.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.2
|
||||||
# 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
|
196
senpy/plugins/postprocessing/emotion/maxEmotion_plugin.py
Normal file
196
senpy/plugins/postprocessing/emotion/maxEmotion_plugin.py
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
from senpy import PostProcessing, easy_test
|
||||||
|
|
||||||
|
|
||||||
|
class MaxEmotion(PostProcessing):
|
||||||
|
'''Plugin to extract the emotion with highest value from an EmotionSet'''
|
||||||
|
author = '@dsuarezsouto'
|
||||||
|
version = '0.1'
|
||||||
|
|
||||||
|
def process_entry(self, entry, params):
|
||||||
|
if len(entry.emotions) < 1:
|
||||||
|
yield entry
|
||||||
|
return
|
||||||
|
|
||||||
|
set_emotions = entry.emotions[0]['onyx:hasEmotion']
|
||||||
|
|
||||||
|
# If there is only one emotion, do not modify it
|
||||||
|
if len(set_emotions) < 2:
|
||||||
|
yield entry
|
||||||
|
return
|
||||||
|
|
||||||
|
max_emotion = set_emotions[0]
|
||||||
|
|
||||||
|
# Extract max emotion from the set emotions (emotion with highest intensity)
|
||||||
|
for tmp_emotion in set_emotions:
|
||||||
|
if tmp_emotion['onyx:hasEmotionIntensity'] > max_emotion[
|
||||||
|
'onyx:hasEmotionIntensity']:
|
||||||
|
max_emotion = tmp_emotion
|
||||||
|
|
||||||
|
if max_emotion['onyx:hasEmotionIntensity'] == 0:
|
||||||
|
max_emotion['onyx:hasEmotionCategory'] = "neutral"
|
||||||
|
max_emotion['onyx:hasEmotionIntensity'] = 1.0
|
||||||
|
|
||||||
|
entry.emotions[0]['onyx:hasEmotion'] = [max_emotion]
|
||||||
|
|
||||||
|
entry.emotions[0]['prov:wasGeneratedBy'] = "maxSentiment"
|
||||||
|
yield entry
|
||||||
|
|
||||||
|
def check(self, request, plugins):
|
||||||
|
return 'maxemotion' in request.parameters and self not in plugins
|
||||||
|
|
||||||
|
# Test Cases:
|
||||||
|
# 1 Normal Situation.
|
||||||
|
# 2 Case to return a Neutral Emotion.
|
||||||
|
test_cases = [
|
||||||
|
{
|
||||||
|
"name":
|
||||||
|
"If there are several emotions within an emotion set, reduce it to one.",
|
||||||
|
"entry": {
|
||||||
|
"@type":
|
||||||
|
"entry",
|
||||||
|
"emotions": [
|
||||||
|
{
|
||||||
|
"@id":
|
||||||
|
"Emotions0",
|
||||||
|
"@type":
|
||||||
|
"emotionSet",
|
||||||
|
"onyx:hasEmotion": [
|
||||||
|
{
|
||||||
|
"@id": "_:Emotion_1538121033.74",
|
||||||
|
"@type": "emotion",
|
||||||
|
"onyx:hasEmotionCategory": "anger",
|
||||||
|
"onyx:hasEmotionIntensity": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@id": "_:Emotion_1538121033.74",
|
||||||
|
"@type": "emotion",
|
||||||
|
"onyx:hasEmotionCategory": "joy",
|
||||||
|
"onyx:hasEmotionIntensity": 0.3333333333333333
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@id": "_:Emotion_1538121033.74",
|
||||||
|
"@type": "emotion",
|
||||||
|
"onyx:hasEmotionCategory": "negative-fear",
|
||||||
|
"onyx:hasEmotionIntensity": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@id": "_:Emotion_1538121033.74",
|
||||||
|
"@type": "emotion",
|
||||||
|
"onyx:hasEmotionCategory": "sadness",
|
||||||
|
"onyx:hasEmotionIntensity": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@id": "_:Emotion_1538121033.74",
|
||||||
|
"@type": "emotion",
|
||||||
|
"onyx:hasEmotionCategory": "disgust",
|
||||||
|
"onyx:hasEmotionIntensity": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nif:isString":
|
||||||
|
"Test"
|
||||||
|
},
|
||||||
|
'expected': {
|
||||||
|
"@type":
|
||||||
|
"entry",
|
||||||
|
"emotions": [
|
||||||
|
{
|
||||||
|
"@id":
|
||||||
|
"Emotions0",
|
||||||
|
"@type":
|
||||||
|
"emotionSet",
|
||||||
|
"onyx:hasEmotion": [
|
||||||
|
{
|
||||||
|
"@id": "_:Emotion_1538121033.74",
|
||||||
|
"@type": "emotion",
|
||||||
|
"onyx:hasEmotionCategory": "joy",
|
||||||
|
"onyx:hasEmotionIntensity": 0.3333333333333333
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"prov:wasGeneratedBy":
|
||||||
|
'maxSentiment'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nif:isString":
|
||||||
|
"Test"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":
|
||||||
|
"If the maximum emotion has an intensity of 0, return a neutral emotion.",
|
||||||
|
"entry": {
|
||||||
|
"@type":
|
||||||
|
"entry",
|
||||||
|
"emotions": [{
|
||||||
|
"@id":
|
||||||
|
"Emotions0",
|
||||||
|
"@type":
|
||||||
|
"emotionSet",
|
||||||
|
"onyx:hasEmotion": [
|
||||||
|
{
|
||||||
|
"@id": "_:Emotion_1538121033.74",
|
||||||
|
"@type": "emotion",
|
||||||
|
"onyx:hasEmotionCategory": "anger",
|
||||||
|
"onyx:hasEmotionIntensity": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@id": "_:Emotion_1538121033.74",
|
||||||
|
"@type": "emotion",
|
||||||
|
"onyx:hasEmotionCategory": "joy",
|
||||||
|
"onyx:hasEmotionIntensity": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@id":
|
||||||
|
"_:Emotion_1538121033.74",
|
||||||
|
"@type":
|
||||||
|
"emotion",
|
||||||
|
"onyx:hasEmotionCategory":
|
||||||
|
"negative-fear",
|
||||||
|
"onyx:hasEmotionIntensity":
|
||||||
|
0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@id": "_:Emotion_1538121033.74",
|
||||||
|
"@type": "emotion",
|
||||||
|
"onyx:hasEmotionCategory":
|
||||||
|
"sadness",
|
||||||
|
"onyx:hasEmotionIntensity": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@id": "_:Emotion_1538121033.74",
|
||||||
|
"@type": "emotion",
|
||||||
|
"onyx:hasEmotionCategory":
|
||||||
|
"disgust",
|
||||||
|
"onyx:hasEmotionIntensity": 0
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
"nif:isString":
|
||||||
|
"Test"
|
||||||
|
},
|
||||||
|
'expected': {
|
||||||
|
"@type":
|
||||||
|
"entry",
|
||||||
|
"emotions": [{
|
||||||
|
"@id":
|
||||||
|
"Emotions0",
|
||||||
|
"@type":
|
||||||
|
"emotionSet",
|
||||||
|
"onyx:hasEmotion": [{
|
||||||
|
"@id": "_:Emotion_1538121033.74",
|
||||||
|
"@type": "emotion",
|
||||||
|
"onyx:hasEmotionCategory": "neutral",
|
||||||
|
"onyx:hasEmotionIntensity": 1
|
||||||
|
}],
|
||||||
|
"prov:wasGeneratedBy":
|
||||||
|
'maxSentiment'
|
||||||
|
}],
|
||||||
|
"nif:isString":
|
||||||
|
"Test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
easy_test()
|
@@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
name: sentiment140
|
|
||||||
module: sentiment140
|
|
||||||
description: "Connects to the sentiment140 free API: http://sentiment140.com"
|
|
||||||
author: "@balkian"
|
|
||||||
version: '0.2'
|
|
||||||
url: "https://github.com/gsi-upm/senpy-plugins-community"
|
|
||||||
extra_params:
|
|
||||||
language:
|
|
||||||
"@id": lang_sentiment140
|
|
||||||
aliases:
|
|
||||||
- language
|
|
||||||
- l
|
|
||||||
required: false
|
|
||||||
options:
|
|
||||||
- es
|
|
||||||
- en
|
|
||||||
- auto
|
|
||||||
default: auto
|
|
||||||
requirements: {}
|
|
||||||
maxPolarityValue: 1
|
|
||||||
minPolarityValue: 0
|
|
@@ -4,12 +4,31 @@ import json
|
|||||||
from senpy.plugins import SentimentPlugin
|
from senpy.plugins import SentimentPlugin
|
||||||
from senpy.models import Sentiment
|
from senpy.models import Sentiment
|
||||||
|
|
||||||
|
ENDPOINT = 'http://www.sentiment140.com/api/bulkClassifyJson'
|
||||||
|
|
||||||
class Sentiment140Plugin(SentimentPlugin):
|
|
||||||
|
class Sentiment140(SentimentPlugin):
|
||||||
'''Connects to the sentiment140 free API: http://sentiment140.com'''
|
'''Connects to the sentiment140 free API: http://sentiment140.com'''
|
||||||
|
|
||||||
|
author = "@balkian"
|
||||||
|
version = '0.2'
|
||||||
|
url = "https://github.com/gsi-upm/senpy-plugins-community"
|
||||||
|
extra_params = {
|
||||||
|
'language': {
|
||||||
|
"@id": 'lang_sentiment140',
|
||||||
|
'aliases': ['language', 'l'],
|
||||||
|
'required': False,
|
||||||
|
'default': 'auto',
|
||||||
|
'options': ['es', 'en', 'auto']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maxPolarityValue = 1
|
||||||
|
minPolarityValue = 0
|
||||||
|
|
||||||
def analyse_entry(self, entry, params):
|
def analyse_entry(self, entry, params):
|
||||||
lang = params["language"]
|
lang = params["language"]
|
||||||
res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson",
|
res = requests.post(ENDPOINT,
|
||||||
json.dumps({
|
json.dumps({
|
||||||
"language": lang,
|
"language": lang,
|
||||||
"data": [{
|
"data": [{
|
||||||
@@ -31,23 +50,10 @@ class Sentiment140Plugin(SentimentPlugin):
|
|||||||
marl__hasPolarity=polarity,
|
marl__hasPolarity=polarity,
|
||||||
marl__polarityValue=polarity_value)
|
marl__polarityValue=polarity_value)
|
||||||
sentiment.prov__wasGeneratedBy = self.id
|
sentiment.prov__wasGeneratedBy = self.id
|
||||||
entry.sentiments = []
|
|
||||||
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": 10}]}
|
|
||||||
with patch_requests(expected) as (request, response):
|
|
||||||
super(Sentiment140Plugin, self).test(*args, **kwargs)
|
|
||||||
assert request.called
|
|
||||||
assert response.json.called
|
|
||||||
|
|
||||||
test_cases = [
|
test_cases = [
|
||||||
{
|
{
|
||||||
'entry': {
|
'entry': {
|
||||||
@@ -61,6 +67,9 @@ class Sentiment140Plugin(SentimentPlugin):
|
|||||||
'marl:hasPolarity': 'marl:Positive',
|
'marl:hasPolarity': 'marl:Positive',
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
'responses': [{'url': ENDPOINT,
|
||||||
|
'method': 'POST',
|
||||||
|
'json': {'data': [{'polarity': 4}]}}]
|
||||||
}
|
}
|
||||||
]
|
]
|
38
senpy/schemas/aggregatedEvaluation.json
Normal file
38
senpy/schemas/aggregatedEvaluation.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
|
"allOf": [
|
||||||
|
{"$ref": "response.json"},
|
||||||
|
{
|
||||||
|
"title": "AggregatedEvaluation",
|
||||||
|
"description": "The results of the evaluation",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"@context": {
|
||||||
|
"$ref": "context.json"
|
||||||
|
},
|
||||||
|
"@type": {
|
||||||
|
"default": "AggregatedEvaluation"
|
||||||
|
},
|
||||||
|
"@id": {
|
||||||
|
"description": "ID of the aggregated evaluation",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"evaluations": {
|
||||||
|
"default": [],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "evaluation.json"
|
||||||
|
},{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
"required": ["@id", "evaluations"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@@ -9,7 +9,20 @@
|
|||||||
"@type": {
|
"@type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Type of the analysis. e.g. marl:SentimentAnalysis"
|
"description": "Type of the analysis. e.g. marl:SentimentAnalysis"
|
||||||
|
},
|
||||||
|
"prov:wasAssociatedWith": {
|
||||||
|
"@type": "string",
|
||||||
|
"description": "Algorithm/plugin that was used"
|
||||||
|
},
|
||||||
|
"prov:used": {
|
||||||
|
"description": "Parameters of the algorithm",
|
||||||
|
"@type": "array",
|
||||||
|
"default": [],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "parameter.json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["@id", "@type"]
|
"required": ["@type", "prov:wasAssociatedWith"]
|
||||||
}
|
}
|
||||||
|
@@ -10,8 +10,10 @@
|
|||||||
"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": "dc:subject"
|
"@id": "nif:topic",
|
||||||
|
"@container": "@set"
|
||||||
},
|
},
|
||||||
"entities": {
|
"entities": {
|
||||||
"@id": "me:hasEntities"
|
"@id": "me:hasEntities"
|
||||||
@@ -39,7 +41,7 @@
|
|||||||
"@container": "@set"
|
"@container": "@set"
|
||||||
},
|
},
|
||||||
"analysis": {
|
"analysis": {
|
||||||
"@id": "AnalysisInvolved",
|
"@id": "prov:wasInformedBy",
|
||||||
"@type": "@id",
|
"@type": "@id",
|
||||||
"@container": "@set"
|
"@container": "@set"
|
||||||
},
|
},
|
||||||
|
29
senpy/schemas/dataset.json
Normal file
29
senpy/schemas/dataset.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
|
"name": "Dataset",
|
||||||
|
"properties": {
|
||||||
|
"@id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"compression": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expected_bytes": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"filename": {
|
||||||
|
"description": "Name of the dataset",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"description": "Classifier or plugin evaluated",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["@id"]
|
||||||
|
}
|
18
senpy/schemas/datasets.json
Normal file
18
senpy/schemas/datasets.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
|
"allOf": [
|
||||||
|
{"$ref": "response.json"},
|
||||||
|
{
|
||||||
|
"required": ["datasets"],
|
||||||
|
"properties": {
|
||||||
|
"datasets": {
|
||||||
|
"type": "array",
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"$ref": "dataset.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@@ -41,5 +41,20 @@
|
|||||||
},
|
},
|
||||||
"Response": {
|
"Response": {
|
||||||
"$ref": "response.json"
|
"$ref": "response.json"
|
||||||
|
},
|
||||||
|
"AggregatedEvaluation": {
|
||||||
|
"$ref": "aggregatedEvaluation.json"
|
||||||
|
},
|
||||||
|
"Evaluation": {
|
||||||
|
"$ref": "evaluation.json"
|
||||||
|
},
|
||||||
|
"Metric": {
|
||||||
|
"$ref": "metric.json"
|
||||||
|
},
|
||||||
|
"Dataset": {
|
||||||
|
"$ref": "dataset.json"
|
||||||
|
},
|
||||||
|
"Datasets": {
|
||||||
|
"$ref": "datasets.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,5 +20,5 @@
|
|||||||
"description": "The ID of the analysis that generated this Emotion. The full object should be included in the \"analysis\" property of the root object"
|
"description": "The ID of the analysis that generated this Emotion. The full object should be included in the \"analysis\" property of the root object"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["@id", "prov:wasGeneratedBy", "onyx:hasEmotion"]
|
"required": ["prov:wasGeneratedBy", "onyx:hasEmotion"]
|
||||||
}
|
}
|
||||||
|
@@ -35,5 +35,5 @@
|
|||||||
"default": []
|
"default": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["@id", "nif:isString"]
|
"required": ["nif:isString"]
|
||||||
}
|
}
|
||||||
|
28
senpy/schemas/evaluation.json
Normal file
28
senpy/schemas/evaluation.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
|
"name": "Evaluation",
|
||||||
|
"properties": {
|
||||||
|
"@id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"@type": {
|
||||||
|
"type": "array",
|
||||||
|
"default": "Evaluation"
|
||||||
|
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "metric.json" },
|
||||||
|
"default": []
|
||||||
|
},
|
||||||
|
"evaluatesOn": {
|
||||||
|
"description": "Name of the dataset evaluated ",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"evaluates": {
|
||||||
|
"description": "Classifier or plugin evaluated",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["@id", "metrics"]
|
||||||
|
}
|
24
senpy/schemas/metric.json
Normal file
24
senpy/schemas/metric.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
|
"properties": {
|
||||||
|
"@id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"@type": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"maxValue": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"minValue": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"deviation": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["@id"]
|
||||||
|
}
|
16
senpy/schemas/parameter.json
Normal file
16
senpy/schemas/parameter.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
|
"description": "Parameters for a senpy analysis",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Name of the parameter"
|
||||||
|
},
|
||||||
|
"prov:value": {
|
||||||
|
"@type": "any",
|
||||||
|
"description": "Value of the parameter"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name", "prov:value"]
|
||||||
|
}
|
@@ -21,13 +21,7 @@
|
|||||||
"default": [],
|
"default": [],
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"anyOf": [
|
"$ref": "analysis.json"
|
||||||
{
|
|
||||||
"$ref": "analysis.json"
|
|
||||||
},{
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entries": {
|
"entries": {
|
||||||
|
@@ -19,5 +19,5 @@
|
|||||||
"description": "The ID of the analysis that generated this Sentiment. The full object should be included in the \"analysis\" property of the root object"
|
"description": "The ID of the analysis that generated this Sentiment. The full object should be included in the \"analysis\" property of the root object"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["@id", "prov:wasGeneratedBy"]
|
"required": ["prov:wasGeneratedBy"]
|
||||||
}
|
}
|
||||||
|
@@ -167,3 +167,36 @@ textarea{
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.collapsed .collapseicon {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed .expandicon {
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandicon {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapseicon {
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
border: 6px solid #f3f3f3; /* Light grey */
|
||||||
|
border-top: 6px solid blue;
|
||||||
|
border-bottom: 6px solid blue;
|
||||||
|
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 3em;
|
||||||
|
height: 3em;
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
@@ -4,6 +4,7 @@ var plugins_params = default_params = {};
|
|||||||
var plugins = [];
|
var plugins = [];
|
||||||
var defaultPlugin = {};
|
var defaultPlugin = {};
|
||||||
var gplugins = {};
|
var gplugins = {};
|
||||||
|
var pipeline = [];
|
||||||
|
|
||||||
function replaceURLWithHTMLLinks(text) {
|
function replaceURLWithHTMLLinks(text) {
|
||||||
console.log('Text: ' + text);
|
console.log('Text: ' + text);
|
||||||
@@ -30,7 +31,14 @@ function hashchanged(){
|
|||||||
|
|
||||||
|
|
||||||
function get_plugins(response){
|
function get_plugins(response){
|
||||||
plugins = response.plugins;
|
for(ix in response.plugins){
|
||||||
|
plug = response.plugins[ix];
|
||||||
|
plugins[plug.name] = plug;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_datasets(response){
|
||||||
|
datasets = response.datasets
|
||||||
}
|
}
|
||||||
|
|
||||||
function group_plugins(){
|
function group_plugins(){
|
||||||
@@ -77,9 +85,34 @@ function draw_plugins_selection(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
html += "</optgroup>"
|
html += "</optgroup>"
|
||||||
document.getElementById('plugins').innerHTML = html;
|
// Two elements with plugin class
|
||||||
|
// One from the evaluate tab and another one from the analyse tab
|
||||||
|
plugin_lists = document.getElementsByClassName('plugin')
|
||||||
|
for (element in plugin_lists){
|
||||||
|
plugin_lists[element].innerHTML = html;
|
||||||
|
}
|
||||||
|
draw_plugin_pipeline();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function draw_plugin_pipeline(){
|
||||||
|
var pipeHTML = "";
|
||||||
|
console.log("Drawing pipeline: ", pipeline);
|
||||||
|
for (ix in pipeline){
|
||||||
|
plug = pipeline[ix];
|
||||||
|
pipeHTML += '<span onclick="remove_plugin_pipeline(\'' + plug + '\')" class="btn btn-primary"><span ><i class="fa fa-minus"></i></span> ' + plug + '</span> <i class="fa fa-arrow-right"></i> ';
|
||||||
|
}
|
||||||
|
console.log(pipeHTML);
|
||||||
|
$("#pipeline").html(pipeHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function remove_plugin_pipeline(name){
|
||||||
|
console.log("Removing plugin: ", name);
|
||||||
|
var index = pipeline.indexOf(name);
|
||||||
|
pipeline.splice(index, 1);
|
||||||
|
draw_plugin_pipeline();
|
||||||
|
|
||||||
|
}
|
||||||
function draw_plugins_list(){
|
function draw_plugins_list(){
|
||||||
var availablePlugins = document.getElementById('availablePlugins');
|
var availablePlugins = document.getElementById('availablePlugins');
|
||||||
|
|
||||||
@@ -98,15 +131,40 @@ function draw_plugins_list(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function add_plugin_pipeline(){
|
||||||
|
var selected = get_selected_plugin();
|
||||||
|
pipeline.push(selected);
|
||||||
|
console.log("Adding ", selected);
|
||||||
|
draw_plugin_pipeline();
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw_datasets(){
|
||||||
|
html = "";
|
||||||
|
repeated_html = "<input class=\"checks-datasets\" type=\"checkbox\" value=\"";
|
||||||
|
for (dataset in datasets){
|
||||||
|
html += repeated_html+datasets[dataset]["@id"]+"\">"+datasets[dataset]["@id"];
|
||||||
|
html += "<br>"
|
||||||
|
}
|
||||||
|
document.getElementById("datasets").innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(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);
|
defaultPlugin= JSON.parse($.ajax({type: "GET", url: "/api/plugins/default" , async: false}).responseText);
|
||||||
|
|
||||||
get_plugins(response);
|
get_plugins(response);
|
||||||
get_default_parameters();
|
get_default_parameters();
|
||||||
|
|
||||||
draw_plugins_list();
|
draw_plugins_list();
|
||||||
draw_plugins_selection();
|
draw_plugins_selection();
|
||||||
draw_parameters();
|
draw_parameters();
|
||||||
|
draw_plugin_description();
|
||||||
|
|
||||||
|
if (evaluation_enabled) {
|
||||||
|
var response2 = JSON.parse($.ajax({type: "GET", url: "/api/datasets/" , async: false}).responseText);
|
||||||
|
get_datasets(response2);
|
||||||
|
draw_datasets();
|
||||||
|
}
|
||||||
|
|
||||||
$(window).on('hashchange', hashchanged);
|
$(window).on('hashchange', hashchanged);
|
||||||
hashchanged();
|
hashchanged();
|
||||||
@@ -123,17 +181,34 @@ function get_default_parameters(){
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function get_selected_plugin(){
|
||||||
|
return document.getElementsByClassName('plugin')[0].options[document.getElementsByClassName('plugin')[0].selectedIndex].value;
|
||||||
|
}
|
||||||
|
|
||||||
function draw_default_parameters(){
|
function draw_default_parameters(){
|
||||||
var basic_params = document.getElementById("basic_params");
|
var basic_params = document.getElementById("basic_params");
|
||||||
basic_params.innerHTML = params_div(default_params);
|
basic_params.innerHTML = params_div(default_params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update_params(params, plug){
|
||||||
|
ep = plugins_params[plug];
|
||||||
|
for(k in ep){
|
||||||
|
params[k] = ep[k];
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
function draw_extra_parameters(){
|
function draw_extra_parameters(){
|
||||||
var plugin = document.getElementById("plugins").options[document.getElementById("plugins").selectedIndex].value;
|
var plugin = get_selected_plugin();
|
||||||
get_parameters();
|
get_parameters();
|
||||||
|
|
||||||
var extra_params = document.getElementById("extra_params");
|
var extra_params = document.getElementById("extra_params");
|
||||||
extra_params.innerHTML = params_div(plugins_params[plugin]);
|
var params = {};
|
||||||
|
for (sel in pipeline){
|
||||||
|
update_params(params, pipeline[sel]);
|
||||||
|
}
|
||||||
|
update_params(params, plugin);
|
||||||
|
extra_params.innerHTML = params_div(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
function draw_parameters(){
|
function draw_parameters(){
|
||||||
@@ -240,13 +315,27 @@ function add_param(key, value){
|
|||||||
return "&"+key+"="+value;
|
return "&"+key+"="+value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function get_pipeline_arg(){
|
||||||
|
arg = "";
|
||||||
|
for (ix in pipeline){
|
||||||
|
arg = arg + pipeline[ix] + ",";
|
||||||
|
}
|
||||||
|
arg = arg + get_selected_plugin();
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function load_JSON(){
|
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 = get_pipeline_arg();
|
||||||
|
$(".loading").addClass("loader");
|
||||||
|
$("#preview").hide();
|
||||||
|
|
||||||
var input = encodeURIComponent(document.getElementById("input").value);
|
var input = encodeURIComponent(document.getElementById("input").value);
|
||||||
url += "?algo="+plugin+"&i="+input
|
url += "?algo="+plugin+"&i="+input
|
||||||
|
|
||||||
@@ -256,25 +345,120 @@ function load_JSON(){
|
|||||||
url += add_param(key, params[key]);
|
url += add_param(key, params[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = $.ajax({type: "GET", url: url , async: false}).responseText;
|
$.ajax({type: "GET", url: url}).always(function(response){
|
||||||
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
|
document.getElementById("results-div").style.display = 'block';
|
||||||
|
if(typeof response=="object") {
|
||||||
|
var options = {
|
||||||
|
mode: 'view'
|
||||||
|
};
|
||||||
|
var editor = new JSONEditor(container, options, response);
|
||||||
|
editor.expandAll();
|
||||||
|
$('#results-div a[href="#viewer"]').click();
|
||||||
|
response = JSON.stringify(response, null, 4);
|
||||||
|
} else {
|
||||||
|
console.log("Got turtle?");
|
||||||
|
$('#results-div a[href="#raw"]').click();
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById("input_request").innerHTML = "<a href='"+url+"'>"+url+"</a>"
|
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
|
||||||
document.getElementById("results-div").style.display = 'block';
|
document.getElementById("input_request").innerHTML = "<a href='"+url+"'>"+url+"</a>"
|
||||||
try {
|
|
||||||
response = JSON.parse(response);
|
$(".loading").removeClass("loader");
|
||||||
var options = {
|
$("#preview").show();
|
||||||
mode: 'view'
|
});
|
||||||
};
|
}
|
||||||
var editor = new JSONEditor(container, options, response);
|
|
||||||
editor.expandAll();
|
function get_datasets_from_checkbox(){
|
||||||
// $('#results-div a[href="#viewer"]').tab('show');
|
var checks = document.getElementsByClassName("checks-datasets");
|
||||||
$('#results-div a[href="#viewer"]').click();
|
|
||||||
// location.hash = 'raw';
|
datasets = "";
|
||||||
}
|
for (var i = 0; i < checks.length; i++){
|
||||||
catch(err){
|
if (checks[i].checked){
|
||||||
console.log("Error decoding JSON (got turtle?)");
|
datasets += checks[i].value + ",";
|
||||||
$('#results-div a[href="#raw"]').click();
|
}
|
||||||
// location.hash = 'raw';
|
}
|
||||||
}
|
datasets = datasets.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function create_body_metrics(evaluations){
|
||||||
|
var new_tbody = document.createElement('tbody')
|
||||||
|
var metric_html = ""
|
||||||
|
for (var eval in evaluations){
|
||||||
|
metric_html += "<tr><th>"+evaluations[eval].evaluates+"</th><th>"+evaluations[eval].evaluatesOn+"</th>";
|
||||||
|
for (var metric in evaluations[eval].metrics){
|
||||||
|
metric_html += "<th>"+parseFloat(evaluations[eval].metrics[metric].value.toFixed(4))+"</th>";
|
||||||
|
}
|
||||||
|
metric_html += "</tr>";
|
||||||
|
}
|
||||||
|
new_tbody.innerHTML = metric_html
|
||||||
|
return new_tbody
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluate_JSON(){
|
||||||
|
|
||||||
|
url = "/api/evaluate";
|
||||||
|
|
||||||
|
var container = document.getElementById('results_eval');
|
||||||
|
var rawcontainer = document.getElementById('jsonraw_eval');
|
||||||
|
var table = document.getElementById("eval_table");
|
||||||
|
|
||||||
|
rawcontainer.innerHTML = "";
|
||||||
|
container.innerHTML = "";
|
||||||
|
|
||||||
|
var plugin = document.getElementsByClassName("plugin")[0].options[document.getElementsByClassName("plugin")[0].selectedIndex].value;
|
||||||
|
|
||||||
|
get_datasets_from_checkbox();
|
||||||
|
|
||||||
|
url += "?algo="+plugin+"&dataset="+datasets
|
||||||
|
|
||||||
|
$('#doevaluate').attr("disabled", true);
|
||||||
|
$.ajax({type: "GET", url: url, dataType: 'json'}).always(function(resp) {
|
||||||
|
$('#doevaluate').attr("disabled", false);
|
||||||
|
response = resp.responseText;
|
||||||
|
|
||||||
|
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
|
||||||
|
|
||||||
|
document.getElementById("input_request_eval").innerHTML = "<a href='"+url+"'>"+url+"</a>"
|
||||||
|
document.getElementById("evaluate-div").style.display = 'block';
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = JSON.parse(response);
|
||||||
|
var options = {
|
||||||
|
mode: 'view'
|
||||||
|
};
|
||||||
|
|
||||||
|
//Control the single response results
|
||||||
|
if (!(Array.isArray(response.evaluations))){
|
||||||
|
response.evaluations = [response.evaluations]
|
||||||
|
}
|
||||||
|
|
||||||
|
new_tbody = create_body_metrics(response.evaluations)
|
||||||
|
table.replaceChild(new_tbody, table.lastElementChild)
|
||||||
|
|
||||||
|
var editor = new JSONEditor(container, options, response);
|
||||||
|
editor.expandAll();
|
||||||
|
// $('#results-div a[href="#viewer"]').tab('show');
|
||||||
|
$('#evaluate-div a[href="#evaluate-table"]').click();
|
||||||
|
// location.hash = 'raw';
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(err){
|
||||||
|
console.log("Error decoding JSON (got turtle?)");
|
||||||
|
$('#evaluate-div a[href="#evaluate-raw"]').click();
|
||||||
|
// location.hash = 'raw';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw_plugin_description(){
|
||||||
|
var plugin = plugins[get_selected_plugin()];
|
||||||
|
$("#plugdescription").text(plugin.description);
|
||||||
|
console.log(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
function plugin_selected(){
|
||||||
|
draw_extra_parameters();
|
||||||
|
draw_plugin_description();
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,9 @@
|
|||||||
<title>Playground {{version}}</title>
|
<title>Playground {{version}}</title>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
<script>
|
||||||
|
this.evaluation_enabled = {% if evaluation %}true{%else %}false{%endif%};
|
||||||
|
</script>
|
||||||
<script src="static/js/jquery-2.1.1.min.js" ></script>
|
<script src="static/js/jquery-2.1.1.min.js" ></script>
|
||||||
<!--<script src="jquery.autosize.min.js"></script>-->
|
<!--<script src="jquery.autosize.min.js"></script>-->
|
||||||
<link rel="stylesheet" href="static/css/bootstrap.min.css">
|
<link rel="stylesheet" href="static/css/bootstrap.min.css">
|
||||||
@@ -32,6 +35,10 @@
|
|||||||
<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>
|
||||||
|
{% if evaluation %}
|
||||||
|
<li role="presentation"><a class="active" href="#evaluate">Evaluate Plugins</a></li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
@@ -54,10 +61,19 @@
|
|||||||
<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>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
<p>Senpy is a research project. If you use it in your research, please cite:
|
||||||
|
<pre>
|
||||||
|
Senpy: A Pragmatic Linked Sentiment Analysis Framework.
|
||||||
|
Sánchez-Rada, J. F., Iglesias, C. A., Corcuera, I., & Araque, Ó.
|
||||||
|
In Data Science and Advanced Analytics (DSAA),
|
||||||
|
2016 IEEE International Conference on (pp. 735-742). IEEE.
|
||||||
|
</pre>
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-6 ">
|
<div class="col-lg-6 ">
|
||||||
@@ -67,8 +83,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-body"><ul id=availablePlugins></ul></div>
|
<div class="panel-body"><ul id=availablePlugins></ul></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="col-lg-6 ">
|
|
||||||
<a href="http://senpy.readthedocs.io">
|
<a href="http://senpy.readthedocs.io">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><i class="fa fa-book"></i> If you are new to senpy, you might want to read senpy's documentation</div>
|
<div class="panel-heading"><i class="fa fa-book"></i> If you are new to senpy, you might want to read senpy's documentation</div>
|
||||||
@@ -79,9 +93,6 @@
|
|||||||
<div class="panel-heading"><i class="fa fa-sign-in"></i> Feel free to follow us on GitHub</div>
|
<div class="panel-heading"><i class="fa fa-sign-in"></i> Feel free to follow us on GitHub</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading"><i class="fa fa-child"></i> Enjoy.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,17 +104,28 @@
|
|||||||
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>
|
||||||
<div>
|
|
||||||
<label>Select the plugin:</label>
|
|
||||||
<select id="plugins" name="plugins" onchange="draw_extra_parameters()">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<!-- PARAMETERS -->
|
<!-- PARAMETERS -->
|
||||||
<div class="panel-group" id="parameters">
|
<div class="panel-group" id="parameters">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<a data-toggle="collapse" class="deco-none" href="#basic_params">
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
Select the plugin.
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="plugin_selection" class="panel-collapse panel-body">
|
||||||
|
<span id="pipeline"></span>
|
||||||
|
<select name="plugins" class="plugin" onchange="plugin_selected()">
|
||||||
|
</select>
|
||||||
|
<span onclick="add_plugin_pipeline()"><span class="btn"><i class="fa fa-plus" title="Add more plugins to the pipeline. Processing order is left to right. i.e. the results of the leftmost plugin will be used as input for the second leftmost, and so on."></i></span></span>
|
||||||
|
<label class="help-block " id="plugdescription"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<a data-toggle="collapse" class="deco-none collapsed" href="#basic_params">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title">
|
<h4 class="panel-title">
|
||||||
|
<i class="fa fa-chevron-right pull-left expandicon"></i>
|
||||||
|
<i class="fa fa-chevron-down pull-left collapseicon"></i>
|
||||||
Basic API parameters
|
Basic API parameters
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,6 +137,8 @@ I cannot believe it!</textarea>
|
|||||||
<a data-toggle="collapse" class="deco-none" href="#extra_params">
|
<a data-toggle="collapse" class="deco-none" href="#extra_params">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title">
|
<h4 class="panel-title">
|
||||||
|
<i class="fa fa-chevron-right pull-left expandicon"></i>
|
||||||
|
<i class="fa fa-chevron-down pull-left collapseicon"></i>
|
||||||
Plugin extra parameters
|
Plugin extra parameters
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,6 +150,7 @@ I cannot believe it!</textarea>
|
|||||||
<!-- END PARAMETERS -->
|
<!-- 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>
|
||||||
|
<div id="loading-results" class="loading"></div>
|
||||||
<!--<button id="visualise" name="type" type="button">Visualise!</button>-->
|
<!--<button id="visualise" name="type" type="button">Visualise!</button>-->
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,6 +176,73 @@ I cannot believe it!</textarea>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if evaluation %}
|
||||||
|
|
||||||
|
<div class="tab-pane" id="evaluate">
|
||||||
|
<div class="well">
|
||||||
|
<form id="form" class="container" onsubmit="return getPlugins();" accept-charset="utf-8">
|
||||||
|
<div>
|
||||||
|
<label>Select the plugin:</label>
|
||||||
|
<select id="plugins-eval" name="plugins-eval" class=plugin onchange="draw_extra_parameters()">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Select the datasets:</label>
|
||||||
|
<div id="datasets" name="datasets" >
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a id="doevaluate" class="btn btn-lg btn-primary" onclick="evaluate_JSON()">Evaluate Plugin!</a>
|
||||||
|
<!--<button id="visualise" name="type" type="button">Visualise!</button>-->
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<span id="input_request_eval"></span>
|
||||||
|
<div id="evaluate-div">
|
||||||
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
|
<li role="presentation" class="active"><a data-toggle="tab" class="active" href="#evaluate-viewer">Viewer</a></li>
|
||||||
|
<li role="presentation"><a data-toggle="tab" class="active" href="#evaluate-raw">Raw</a></li>
|
||||||
|
<li role="presentation"><a data-toggle="tab" class="active" href="#evaluate-table">Table</a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content" id="evaluate-container">
|
||||||
|
|
||||||
|
<div class="tab-pane active" id="evaluate-viewer">
|
||||||
|
<div id="content">
|
||||||
|
<pre id="results_eval" class="results_eval"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane" id="evaluate-raw">
|
||||||
|
<div id="content">
|
||||||
|
<pre id="jsonraw_eval" class="results_eval"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane" id="evaluate-table">
|
||||||
|
<table id="eval_table" class="table table-condensed">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Plugin</th>
|
||||||
|
<th>Dataset</th>
|
||||||
|
<th>Accuracy</th>
|
||||||
|
<th>Precision_macro</th>
|
||||||
|
<th>Recall_macro</th>
|
||||||
|
<th>F1_macro</th>
|
||||||
|
<th>F1_weighted</th>
|
||||||
|
<th>F1_micro</th>
|
||||||
|
<th>F1</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<a href="http://www.gsi.dit.upm.es" target="_blank"><img class="center-block" src="static/img/gsi.png"/> </a>
|
<a href="http://www.gsi.dit.upm.es" target="_blank"><img class="center-block" src="static/img/gsi.png"/> </a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,30 +0,0 @@
|
|||||||
try:
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
except ImportError:
|
|
||||||
from mock import patch, MagicMock
|
|
||||||
|
|
||||||
|
|
||||||
import json
|
|
||||||
from contextlib import contextmanager
|
|
||||||
|
|
||||||
from .models import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def patch_requests(value, code=200):
|
|
||||||
success = MagicMock()
|
|
||||||
if isinstance(value, BaseModel):
|
|
||||||
value = value.jsonld()
|
|
||||||
data = json.dumps(value)
|
|
||||||
|
|
||||||
success.json.return_value = value
|
|
||||||
success.data.return_value = data
|
|
||||||
success.status_code = code
|
|
||||||
|
|
||||||
success.content = json.dumps(value)
|
|
||||||
method_mocker = MagicMock()
|
|
||||||
method_mocker.return_value = success
|
|
||||||
with patch.multiple('requests', request=method_mocker,
|
|
||||||
get=method_mocker, post=method_mocker):
|
|
||||||
yield method_mocker, success
|
|
||||||
assert method_mocker.called
|
|
31
senpy/testing.py
Normal file
31
senpy/testing.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from past.builtins import basestring
|
||||||
|
|
||||||
|
import os
|
||||||
|
import responses as requestmock
|
||||||
|
|
||||||
|
from .models import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
MOCK_REQUESTS = os.environ.get('MOCK_REQUESTS', '').lower() in ['no', 'false']
|
||||||
|
|
||||||
|
|
||||||
|
def patch_all_requests(responses):
|
||||||
|
|
||||||
|
patched = requestmock.RequestsMock()
|
||||||
|
|
||||||
|
for response in responses or []:
|
||||||
|
args = response.copy()
|
||||||
|
if 'json' in args and isinstance(args['json'], BaseModel):
|
||||||
|
args['json'] = args['json'].jsonld()
|
||||||
|
args['method'] = getattr(requestmock, args.get('method', 'GET'))
|
||||||
|
patched.add(**args)
|
||||||
|
return patched
|
||||||
|
|
||||||
|
|
||||||
|
def patch_requests(url, response, method='GET', status=200):
|
||||||
|
args = {'url': url, 'method': method, 'status': status}
|
||||||
|
if isinstance(response, basestring):
|
||||||
|
args['body'] = response
|
||||||
|
else:
|
||||||
|
args['json'] = response
|
||||||
|
return patch_all_requests([args])
|
@@ -1,6 +1,7 @@
|
|||||||
from . import models, __version__
|
from . import models, __version__
|
||||||
from collections import MutableMapping
|
from collections import MutableMapping
|
||||||
import pprint
|
import pprint
|
||||||
|
import pdb
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -32,8 +33,8 @@ def check_template(indict, template):
|
|||||||
if indict != template:
|
if indict != template:
|
||||||
raise models.Error(('Differences found.\n'
|
raise models.Error(('Differences found.\n'
|
||||||
'\tExpected: {}\n'
|
'\tExpected: {}\n'
|
||||||
'\tFound: {}').format(pprint.pformat(indict),
|
'\tFound: {}').format(pprint.pformat(template),
|
||||||
pprint.pformat(template)))
|
pprint.pformat(indict)))
|
||||||
|
|
||||||
|
|
||||||
def convert_dictionary(original, mappings):
|
def convert_dictionary(original, mappings):
|
||||||
@@ -67,17 +68,23 @@ def easy_load(app=None, plugin_list=None, plugin_folder=None, **kwargs):
|
|||||||
return sp, app
|
return sp, app
|
||||||
|
|
||||||
|
|
||||||
def easy_test(plugin_list=None):
|
def easy_test(plugin_list=None, debug=True):
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
logging.getLogger().setLevel(logging.INFO)
|
logging.getLogger().setLevel(logging.INFO)
|
||||||
if not plugin_list:
|
try:
|
||||||
from . import plugins
|
if not plugin_list:
|
||||||
import __main__
|
import __main__
|
||||||
plugin_list = plugins.from_module(__main__)
|
logger.info('Loading classes from {}'.format(__main__))
|
||||||
for plug in plugin_list:
|
from . import plugins
|
||||||
plug.test()
|
plugin_list = plugins.from_module(__main__)
|
||||||
logger.info('The tests for {} passed!'.format(plug.name))
|
for plug in plugin_list:
|
||||||
logger.info('All tests passed!')
|
plug.test()
|
||||||
|
plug.log.info('My tests passed!')
|
||||||
|
logger.info('All tests passed for {} plugins!'.format(len(plugin_list)))
|
||||||
|
except Exception:
|
||||||
|
if not debug:
|
||||||
|
raise
|
||||||
|
pdb.post_mortem()
|
||||||
|
|
||||||
|
|
||||||
def easy(host='0.0.0.0', port=5000, debug=True, **kwargs):
|
def easy(host='0.0.0.0', port=5000, debug=True, **kwargs):
|
||||||
|
@@ -12,6 +12,7 @@ max-line-length = 100
|
|||||||
universal=1
|
universal=1
|
||||||
[tool:pytest]
|
[tool:pytest]
|
||||||
addopts = --cov=senpy --cov-report term-missing
|
addopts = --cov=senpy --cov-report term-missing
|
||||||
|
filterwarnings =
|
||||||
|
ignore:the matrix subclass:PendingDeprecationWarning
|
||||||
[coverage:report]
|
[coverage:report]
|
||||||
omit = senpy/__main__.py
|
omit = senpy/__main__.py
|
27
setup.py
27
setup.py
@@ -1,23 +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
|
|
||||||
|
|
||||||
with open('senpy/VERSION') as f:
|
with open('senpy/VERSION') as f:
|
||||||
__version__ = f.read().strip()
|
__version__ = f.read().strip()
|
||||||
assert __version__
|
assert __version__
|
||||||
|
|
||||||
try:
|
|
||||||
install_reqs = parse_requirements(
|
|
||||||
"requirements.txt", session=pip.download.PipSession())
|
|
||||||
test_reqs = parse_requirements(
|
|
||||||
"test-requirements.txt", session=pip.download.PipSession())
|
|
||||||
except AttributeError:
|
|
||||||
install_reqs = parse_requirements("requirements.txt")
|
|
||||||
test_reqs = parse_requirements("test-requirements.txt")
|
|
||||||
|
|
||||||
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(
|
||||||
@@ -38,9 +35,7 @@ setup(
|
|||||||
tests_require=test_reqs,
|
tests_require=test_reqs,
|
||||||
setup_requires=['pytest-runner', ],
|
setup_requires=['pytest-runner', ],
|
||||||
extras_require={
|
extras_require={
|
||||||
'evaluation': [
|
'evaluation': extra_reqs
|
||||||
'gsitk'
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
entry_points={
|
entry_points={
|
||||||
|
@@ -3,8 +3,9 @@ import logging
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from senpy.api import parse_params, API_PARAMS, NIF_PARAMS, WEB_PARAMS
|
from senpy.api import (boolean, parse_params, get_extra_params, parse_analysis,
|
||||||
from senpy.models import Error
|
API_PARAMS, NIF_PARAMS, WEB_PARAMS)
|
||||||
|
from senpy.models import Error, Plugin
|
||||||
|
|
||||||
|
|
||||||
class APITest(TestCase):
|
class APITest(TestCase):
|
||||||
@@ -32,7 +33,7 @@ class APITest(TestCase):
|
|||||||
query = {}
|
query = {}
|
||||||
plug_params = {
|
plug_params = {
|
||||||
'hello': {
|
'hello': {
|
||||||
'aliases': ['hello', 'hiya'],
|
'aliases': ['hiya', 'hello'],
|
||||||
'required': True
|
'required': True
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,6 +49,26 @@ class APITest(TestCase):
|
|||||||
assert 'hello' in p
|
assert 'hello' 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': {
|
||||||
@@ -69,3 +90,156 @@ class APITest(TestCase):
|
|||||||
assert "Dummy" in p['algorithm']
|
assert "Dummy" in p['algorithm']
|
||||||
assert 'input' in p
|
assert 'input' in p
|
||||||
assert p['input'] == 'Aloha my friend'
|
assert p['input'] == 'Aloha my friend'
|
||||||
|
|
||||||
|
def test_parse_analysis(self):
|
||||||
|
'''The API should parse user parameters and return them in a format that plugins can use'''
|
||||||
|
plugins = [
|
||||||
|
Plugin({
|
||||||
|
'name': 'plugin1',
|
||||||
|
'extra_params': {
|
||||||
|
# Incompatible parameter
|
||||||
|
'param0': {
|
||||||
|
'aliases': ['p1', 'parameter1'],
|
||||||
|
'options': ['option1', 'option2'],
|
||||||
|
'default': 'option1',
|
||||||
|
'required': True
|
||||||
|
},
|
||||||
|
'param1': {
|
||||||
|
'aliases': ['p1', 'parameter1'],
|
||||||
|
'options': ['en', 'es'],
|
||||||
|
|
||||||
|
'default': 'en',
|
||||||
|
'required': False
|
||||||
|
},
|
||||||
|
'param2': {
|
||||||
|
'aliases': ['p2', 'parameter2'],
|
||||||
|
'required': False,
|
||||||
|
'options': ['value2_1', 'value2_2', 'value3_3']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}), Plugin({
|
||||||
|
'name': 'plugin2',
|
||||||
|
'extra_params': {
|
||||||
|
'param0': {
|
||||||
|
'aliases': ['parameter1'],
|
||||||
|
'options': ['new option', 'new option2'],
|
||||||
|
'default': 'new option',
|
||||||
|
'required': False
|
||||||
|
},
|
||||||
|
'param1': {
|
||||||
|
'aliases': ['myparam1', 'p1'],
|
||||||
|
'options': ['en', 'de', 'auto'],
|
||||||
|
'default': 'de',
|
||||||
|
'required': True
|
||||||
|
},
|
||||||
|
'param3': {
|
||||||
|
'aliases': ['p3', 'parameter3'],
|
||||||
|
'options': boolean,
|
||||||
|
'default': True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
call = {
|
||||||
|
'param1': 'en',
|
||||||
|
'0.param0': 'option1',
|
||||||
|
'0.param1': 'en',
|
||||||
|
'param2': 'value2_1',
|
||||||
|
'param0': 'new option',
|
||||||
|
'1.param1': 'de',
|
||||||
|
'param3': False,
|
||||||
|
}
|
||||||
|
expected = [
|
||||||
|
{
|
||||||
|
'param0': 'option1',
|
||||||
|
'param1': 'en',
|
||||||
|
'param2': 'value2_1',
|
||||||
|
}, {
|
||||||
|
'param0': 'new option',
|
||||||
|
'param1': 'de',
|
||||||
|
'param3': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
p = parse_analysis(call, plugins)
|
||||||
|
for i, arg in enumerate(expected):
|
||||||
|
params = p[i].params
|
||||||
|
for k, v in arg.items():
|
||||||
|
assert params[k] == v
|
||||||
|
|
||||||
|
def test_get_extra_params(self):
|
||||||
|
'''The API should return the list of valid parameters for a set of plugins'''
|
||||||
|
plugins = [
|
||||||
|
Plugin({
|
||||||
|
'name': 'plugin1',
|
||||||
|
'extra_params': {
|
||||||
|
# Incompatible parameter
|
||||||
|
'param0': {
|
||||||
|
'aliases': ['p1', 'parameter1'],
|
||||||
|
'options': ['option1', 'option2'],
|
||||||
|
'default': 'option1',
|
||||||
|
'required': True
|
||||||
|
},
|
||||||
|
'param1': {
|
||||||
|
'aliases': ['p1', 'parameter1'],
|
||||||
|
'options': ['en', 'es'],
|
||||||
|
'default': 'en',
|
||||||
|
'required': False
|
||||||
|
},
|
||||||
|
'param2': {
|
||||||
|
'aliases': ['p2', 'parameter2'],
|
||||||
|
'required': False,
|
||||||
|
'options': ['value2_1', 'value2_2', 'value3_3']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}), Plugin({
|
||||||
|
'name': 'plugin2',
|
||||||
|
'extra_params': {
|
||||||
|
'param0': {
|
||||||
|
'aliases': ['parameter1'],
|
||||||
|
'options': ['new option', 'new option2'],
|
||||||
|
'default': 'new option',
|
||||||
|
'required': False
|
||||||
|
},
|
||||||
|
'param1': {
|
||||||
|
'aliases': ['myparam1', 'p1'],
|
||||||
|
'options': ['en', 'de', 'auto'],
|
||||||
|
'default': 'de',
|
||||||
|
'required': True
|
||||||
|
},
|
||||||
|
'param3': {
|
||||||
|
'aliases': ['p3', 'parameter3'],
|
||||||
|
'options': boolean,
|
||||||
|
'default': True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
# Overlapping parameters
|
||||||
|
'plugin1.param0': plugins[0]['extra_params']['param0'],
|
||||||
|
'plugin1.param1': plugins[0]['extra_params']['param1'],
|
||||||
|
'plugin2.param0': plugins[1]['extra_params']['param0'],
|
||||||
|
'plugin2.param1': plugins[1]['extra_params']['param1'],
|
||||||
|
|
||||||
|
# Non-overlapping parameters
|
||||||
|
'param2': plugins[0]['extra_params']['param2'],
|
||||||
|
'param3': plugins[1]['extra_params']['param3'],
|
||||||
|
|
||||||
|
# Intersection of overlapping parameters
|
||||||
|
'param1': {
|
||||||
|
'aliases': ['p1'],
|
||||||
|
'options': ['en'],
|
||||||
|
'default': None,
|
||||||
|
'required': True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = get_extra_params(plugins)
|
||||||
|
|
||||||
|
for ik, iv in expected.items():
|
||||||
|
assert ik in result
|
||||||
|
for jk, jv in iv.items():
|
||||||
|
assert jk in result[ik]
|
||||||
|
assert expected[ik][jk] == result[ik][jk]
|
||||||
|
@@ -21,27 +21,29 @@ class BlueprintsTest(TestCase):
|
|||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
"""Set up only once, and re-use in every individual test"""
|
"""Set up only once, and re-use in every individual test"""
|
||||||
cls.app = Flask("test_extensions")
|
cls.app = Flask("test_extensions")
|
||||||
cls.app.debug = False
|
|
||||||
cls.client = cls.app.test_client()
|
cls.client = cls.app.test_client()
|
||||||
cls.senpy = Senpy(default_plugins=True)
|
cls.senpy = Senpy(default_plugins=True)
|
||||||
cls.senpy.init_app(cls.app)
|
cls.senpy.init_app(cls.app)
|
||||||
cls.dir = os.path.join(os.path.dirname(__file__), "..")
|
cls.dir = os.path.join(os.path.dirname(__file__), "..")
|
||||||
cls.senpy.add_folder(cls.dir)
|
cls.senpy.add_folder(cls.dir)
|
||||||
cls.senpy.activate_plugin("Dummy", sync=True)
|
cls.senpy.activate_all()
|
||||||
cls.senpy.activate_plugin("DummyRequired", sync=True)
|
|
||||||
cls.senpy.default_plugin = 'Dummy'
|
cls.senpy.default_plugin = 'Dummy'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.app.config['TESTING'] = True # Tell Flask not to catch Exceptions
|
||||||
|
|
||||||
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):
|
def test_playground(self):
|
||||||
resp = self.client.get("/")
|
resp = self.client.get("/")
|
||||||
assert "main.js" in resp.data.decode()
|
assert "main.js" in resp.get_data(as_text=True)
|
||||||
|
|
||||||
def test_home(self):
|
def test_home(self):
|
||||||
"""
|
"""
|
||||||
Calling with no arguments should ask the user for more arguments
|
Calling with no arguments should ask the user for more arguments
|
||||||
"""
|
"""
|
||||||
|
self.app.config['TESTING'] = False # Errors are expected in this case
|
||||||
resp = self.client.get("/api/")
|
resp = self.client.get("/api/")
|
||||||
self.assertCode(resp, 400)
|
self.assertCode(resp, 400)
|
||||||
js = parse_resp(resp)
|
js = parse_resp(resp)
|
||||||
@@ -64,10 +66,25 @@ class BlueprintsTest(TestCase):
|
|||||||
logging.debug("Got response: %s", js)
|
logging.debug("Got response: %s", js)
|
||||||
assert "@context" in js
|
assert "@context" in js
|
||||||
assert "entries" in js
|
assert "entries" in js
|
||||||
|
assert len(js['analysis']) == 1
|
||||||
|
|
||||||
|
def test_analysis_post(self):
|
||||||
|
"""
|
||||||
|
The results for a POST request should be the same as for a GET request.
|
||||||
|
"""
|
||||||
|
resp = self.client.post("/api/", data={'i': 'My aloha mohame',
|
||||||
|
'algorithm': 'rand',
|
||||||
|
'with_parameters': True})
|
||||||
|
self.assertCode(resp, 200)
|
||||||
|
js = parse_resp(resp)
|
||||||
|
logging.debug("Got response: %s", js)
|
||||||
|
assert "@context" in js
|
||||||
|
assert "entries" in js
|
||||||
|
assert len(js['analysis']) == 1
|
||||||
|
|
||||||
def test_analysis_extra(self):
|
def test_analysis_extra(self):
|
||||||
"""
|
"""
|
||||||
Extra params that have a default should
|
Extra params that have a default should use it
|
||||||
"""
|
"""
|
||||||
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&with_parameters=true")
|
||||||
self.assertCode(resp, 200)
|
self.assertCode(resp, 200)
|
||||||
@@ -81,7 +98,7 @@ 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
|
self.app.config['TESTING'] = False # Errors are expected in this case
|
||||||
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)
|
||||||
@@ -89,15 +106,117 @@ class BlueprintsTest(TestCase):
|
|||||||
assert isinstance(js, models.Error)
|
assert isinstance(js, models.Error)
|
||||||
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired&example=notvalid")
|
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired&example=notvalid")
|
||||||
self.assertCode(resp, 400)
|
self.assertCode(resp, 400)
|
||||||
|
self.app.config['TESTING'] = True
|
||||||
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired&example=a")
|
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired&example=a")
|
||||||
self.assertCode(resp, 200)
|
self.assertCode(resp, 200)
|
||||||
|
|
||||||
|
def test_analysis_url(self):
|
||||||
|
"""
|
||||||
|
The algorithm can also be specified as part of the URL
|
||||||
|
"""
|
||||||
|
self.app.config['TESTING'] = False # Errors are expected in this case
|
||||||
|
resp = self.client.get("/api/DummyRequired?i=My aloha mohame")
|
||||||
|
self.assertCode(resp, 400)
|
||||||
|
js = parse_resp(resp)
|
||||||
|
logging.debug("Got response: %s", js)
|
||||||
|
assert isinstance(js, models.Error)
|
||||||
|
resp = self.client.get("/api/DummyRequired?i=My aloha mohame&example=notvalid")
|
||||||
|
self.assertCode(resp, 400)
|
||||||
|
resp = self.client.get("/api/DummyRequired?i=My aloha mohame&example=a")
|
||||||
|
self.assertCode(resp, 200)
|
||||||
|
|
||||||
|
def test_analysis_chain(self):
|
||||||
|
"""
|
||||||
|
More than one algorithm can be specified. Plugins will then be chained
|
||||||
|
"""
|
||||||
|
resp = self.client.get("/api/Dummy?i=My aloha mohame")
|
||||||
|
js = parse_resp(resp)
|
||||||
|
assert len(js['analysis']) == 1
|
||||||
|
assert js['entries'][0]['nif:isString'] == 'My aloha mohame'[::-1]
|
||||||
|
|
||||||
|
resp = self.client.get("/api/Dummy/Dummy?i=My aloha mohame")
|
||||||
|
# Calling dummy twice, should return the same string
|
||||||
|
self.assertCode(resp, 200)
|
||||||
|
js = parse_resp(resp)
|
||||||
|
assert len(js['analysis']) == 2
|
||||||
|
assert js['entries'][0]['nif:isString'] == 'My aloha mohame'
|
||||||
|
|
||||||
|
resp = self.client.get("/api/Dummy+Dummy?i=My aloha mohame")
|
||||||
|
# Same with pluses instead of slashes
|
||||||
|
self.assertCode(resp, 200)
|
||||||
|
js = parse_resp(resp)
|
||||||
|
assert len(js['analysis']) == 2
|
||||||
|
assert js['entries'][0]['nif:isString'] == 'My aloha mohame'
|
||||||
|
|
||||||
|
def test_analysis_chain_required(self):
|
||||||
|
"""
|
||||||
|
If a parameter is required and duplicated (because two plugins require it), specifying
|
||||||
|
it once should suffice
|
||||||
|
"""
|
||||||
|
resp = self.client.get("/api/DummyRequired/DummyRequired?i=My aloha mohame&example=a")
|
||||||
|
js = parse_resp(resp)
|
||||||
|
assert len(js['analysis']) == 2
|
||||||
|
assert js['entries'][0]['nif:isString'] == 'My aloha mohame'
|
||||||
|
assert js['entries'][0]['reversed'] == 2
|
||||||
|
|
||||||
|
def test_requirements_chain_help(self):
|
||||||
|
'''The extra parameters of each plugin should be merged if they are in a chain '''
|
||||||
|
resp = self.client.get("/api/split/DummyRequired?help=true")
|
||||||
|
self.assertCode(resp, 200)
|
||||||
|
js = parse_resp(resp)
|
||||||
|
assert 'valid_parameters' in js
|
||||||
|
vp = js['valid_parameters']
|
||||||
|
assert 'example' in vp
|
||||||
|
assert 'delimiter' in vp
|
||||||
|
|
||||||
|
def test_requirements_chain_repeat_help(self):
|
||||||
|
'''
|
||||||
|
If a plugin appears several times in a chain, there should be a way to set different
|
||||||
|
parameters for each.
|
||||||
|
'''
|
||||||
|
resp = self.client.get("/api/split/split?help=true")
|
||||||
|
self.assertCode(resp, 200)
|
||||||
|
js = parse_resp(resp)
|
||||||
|
assert 'valid_parameters' in js
|
||||||
|
vp = js['valid_parameters']
|
||||||
|
assert 'delimiter' in vp
|
||||||
|
|
||||||
|
resp = self.client.get("/api/split/split?help=true&verbose=false")
|
||||||
|
js = parse_resp(resp)
|
||||||
|
vp = js['valid_parameters']
|
||||||
|
assert len(vp.keys()) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_requirements_chain(self):
|
||||||
|
"""
|
||||||
|
It should be possible to specify different parameters for each step in the chain.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# First, we split by sentence twice. Each call should generate 3 additional entries
|
||||||
|
# (one per sentence in the original).
|
||||||
|
resp = self.client.get('/api/split/split?i=The first sentence. The second sentence.'
|
||||||
|
'\nA new paragraph&delimiter=sentence')
|
||||||
|
js = parse_resp(resp)
|
||||||
|
assert len(js['analysis']) == 2
|
||||||
|
assert len(js['entries']) == 7
|
||||||
|
|
||||||
|
# Now, we split by sentence. This produces 3 additional entries.
|
||||||
|
# Then, we split by paragraph. This should create 2 additional entries (One per paragraph
|
||||||
|
# in the original text)
|
||||||
|
resp = self.client.get('/api/split/split?i=The first sentence. The second sentence.'
|
||||||
|
'\nA new paragraph&0.delimiter=sentence&1.delimiter=paragraph')
|
||||||
|
# Calling dummy twice, should return the same string
|
||||||
|
self.assertCode(resp, 200)
|
||||||
|
js = parse_resp(resp)
|
||||||
|
assert len(js['analysis']) == 2
|
||||||
|
assert len(js['entries']) == 6
|
||||||
|
|
||||||
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
|
self.app.config['TESTING'] = False # Errors are expected in this case
|
||||||
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)
|
||||||
@@ -139,7 +258,7 @@ class BlueprintsTest(TestCase):
|
|||||||
js = parse_resp(resp)
|
js = parse_resp(resp)
|
||||||
logging.debug(js)
|
logging.debug(js)
|
||||||
assert "@id" in js
|
assert "@id" in js
|
||||||
assert js["@id"] == "plugins/Dummy_0.1"
|
assert js["@id"] == "endpoint:plugins/Dummy_0.1"
|
||||||
|
|
||||||
def test_default(self):
|
def test_default(self):
|
||||||
""" Show only one plugin"""
|
""" Show only one plugin"""
|
||||||
@@ -148,7 +267,7 @@ class BlueprintsTest(TestCase):
|
|||||||
js = parse_resp(resp)
|
js = parse_resp(resp)
|
||||||
logging.debug(js)
|
logging.debug(js)
|
||||||
assert "@id" in js
|
assert "@id" in js
|
||||||
assert js["@id"] == "plugins/Dummy_0.1"
|
assert js["@id"] == "endpoint:plugins/Dummy_0.1"
|
||||||
|
|
||||||
def test_context(self):
|
def test_context(self):
|
||||||
resp = self.client.get("/api/contexts/context.jsonld")
|
resp = self.client.get("/api/contexts/context.jsonld")
|
||||||
@@ -172,5 +291,6 @@ class BlueprintsTest(TestCase):
|
|||||||
assert "help" in js["valid_parameters"]
|
assert "help" in js["valid_parameters"]
|
||||||
|
|
||||||
def test_conversion(self):
|
def test_conversion(self):
|
||||||
|
self.app.config['TESTING'] = False # Errors are expected in this case
|
||||||
resp = self.client.get("/api/?input=hello&algo=emoRand&emotionModel=DOES NOT EXIST")
|
resp = self.client.get("/api/?input=hello&algo=emoRand&emotionModel=DOES NOT EXIST")
|
||||||
self.assertCode(resp, 404)
|
self.assertCode(resp, 404)
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
from senpy.test import patch_requests
|
from senpy.testing 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
|
||||||
@@ -14,22 +14,28 @@ 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):
|
with patch_requests('http://dummy/', Results()):
|
||||||
resp = client.analyse('hello')
|
resp = client.analyse('hello')
|
||||||
assert isinstance(resp, Results)
|
assert isinstance(resp, Results)
|
||||||
request.assert_called_with(
|
with patch_requests('http://dummy/', Error('Nothing')):
|
||||||
url=endpoint + '/', method='GET', params={'input': 'hello'})
|
|
||||||
with patch_requests(Error('Nothing')) as (request, response):
|
|
||||||
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(
|
|
||||||
url=endpoint + '/',
|
def test_client_post(self):
|
||||||
method='GET',
|
endpoint = 'http://dummy/'
|
||||||
params={'input': 'hello',
|
client = Client(endpoint)
|
||||||
'algorithm': 'NONEXISTENT'})
|
with patch_requests('http://dummy/', Results()):
|
||||||
|
resp = client.analyse('hello')
|
||||||
|
assert isinstance(resp, Results)
|
||||||
|
with patch_requests('http://dummy/', Error('Nothing'), method='POST'):
|
||||||
|
try:
|
||||||
|
client.analyse(input='hello', method='POST', algorithm='NONEXISTENT')
|
||||||
|
raise Exception('Exceptions should be raised. This is not golang')
|
||||||
|
except Error:
|
||||||
|
pass
|
||||||
|
|
||||||
def test_plugins(self):
|
def test_plugins(self):
|
||||||
endpoint = 'http://dummy/'
|
endpoint = 'http://dummy/'
|
||||||
@@ -37,11 +43,8 @@ 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):
|
with patch_requests('http://dummy/plugins', plugins):
|
||||||
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(
|
|
||||||
url=endpoint + '/plugins', method='GET',
|
|
||||||
params={})
|
|
||||||
|
@@ -11,14 +11,15 @@ 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 import plugins
|
||||||
from senpy.models import Error, Results, Entry, EmotionSet, Emotion, Plugin
|
from senpy.models import Analysis, Error, Results, Entry, EmotionSet, Emotion, Plugin
|
||||||
from senpy import api
|
from senpy import api
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
|
|
||||||
def analyse(instance, **kwargs):
|
def analyse(instance, **kwargs):
|
||||||
request = api.parse_call(kwargs)
|
basic = api.parse_params(kwargs, api.API_PARAMS)
|
||||||
|
request = api.parse_call(basic)
|
||||||
return instance.analyse(request)
|
return instance.analyse(request)
|
||||||
|
|
||||||
|
|
||||||
@@ -47,11 +48,11 @@ class ExtensionsTest(TestCase):
|
|||||||
|
|
||||||
def test_add_delete(self):
|
def test_add_delete(self):
|
||||||
'''Should be able to add and delete new plugins. '''
|
'''Should be able to add and delete new plugins. '''
|
||||||
new = plugins.Plugin(name='new', description='new', version=0)
|
new = plugins.Analysis(name='new', description='new', version=0)
|
||||||
self.senpy.add_plugin(new)
|
self.senpy.add_plugin(new)
|
||||||
assert new in self.senpy.plugins()
|
assert new in self.senpy.plugins(is_activated=False)
|
||||||
self.senpy.delete_plugin(new)
|
self.senpy.delete_plugin(new)
|
||||||
assert new not in self.senpy.plugins()
|
assert new not in self.senpy.plugins(is_activated=False)
|
||||||
|
|
||||||
def test_adding_folder(self):
|
def test_adding_folder(self):
|
||||||
""" It should be possible for senpy to look for plugins in more folders. """
|
""" It should be possible for senpy to look for plugins in more folders. """
|
||||||
@@ -60,7 +61,7 @@ class ExtensionsTest(TestCase):
|
|||||||
default_plugins=False)
|
default_plugins=False)
|
||||||
assert not senpy.analysis_plugins
|
assert not senpy.analysis_plugins
|
||||||
senpy.add_folder(self.examples_dir)
|
senpy.add_folder(self.examples_dir)
|
||||||
assert senpy.analysis_plugins
|
assert senpy.plugins(plugin_type=plugins.AnalysisPlugin, is_activated=False)
|
||||||
self.assertRaises(AttributeError, senpy.add_folder, 'DOES NOT EXIST')
|
self.assertRaises(AttributeError, senpy.add_folder, 'DOES NOT EXIST')
|
||||||
|
|
||||||
def test_installing(self):
|
def test_installing(self):
|
||||||
@@ -121,8 +122,8 @@ class ExtensionsTest(TestCase):
|
|||||||
# Leaf (defaultdict with __setattr__ and __getattr__.
|
# Leaf (defaultdict with __setattr__ and __getattr__.
|
||||||
r1 = analyse(self.senpy, algorithm="Dummy", input="tupni", output="tuptuo")
|
r1 = analyse(self.senpy, algorithm="Dummy", input="tupni", output="tuptuo")
|
||||||
r2 = analyse(self.senpy, input="tupni", output="tuptuo")
|
r2 = analyse(self.senpy, input="tupni", output="tuptuo")
|
||||||
assert r1.analysis[0] == "plugins/Dummy_0.1"
|
assert r1.analysis[0].algorithm == "endpoint:plugins/Dummy_0.1"
|
||||||
assert r2.analysis[0] == "plugins/Dummy_0.1"
|
assert r2.analysis[0].algorithm == "endpoint:plugins/Dummy_0.1"
|
||||||
assert r1.entries[0]['nif:isString'] == 'input'
|
assert r1.entries[0]['nif:isString'] == 'input'
|
||||||
|
|
||||||
def test_analyse_empty(self):
|
def test_analyse_empty(self):
|
||||||
@@ -130,7 +131,7 @@ class ExtensionsTest(TestCase):
|
|||||||
senpy = Senpy(plugin_folder=None,
|
senpy = Senpy(plugin_folder=None,
|
||||||
app=self.app,
|
app=self.app,
|
||||||
default_plugins=False)
|
default_plugins=False)
|
||||||
self.assertRaises(Error, senpy.analyse, Results())
|
self.assertRaises(Error, senpy.analyse, Results(), [])
|
||||||
|
|
||||||
def test_analyse_wrong(self):
|
def test_analyse_wrong(self):
|
||||||
""" Trying to analyse with a non-existent plugin should raise an error."""
|
""" Trying to analyse with a non-existent plugin should raise an error."""
|
||||||
@@ -156,34 +157,35 @@ class ExtensionsTest(TestCase):
|
|||||||
r2 = analyse(self.senpy,
|
r2 = analyse(self.senpy,
|
||||||
input="tupni",
|
input="tupni",
|
||||||
output="tuptuo")
|
output="tuptuo")
|
||||||
assert r1.analysis[0] == "plugins/Dummy_0.1"
|
assert r1.analysis[0].algorithm == "endpoint:plugins/Dummy_0.1"
|
||||||
assert r2.analysis[0] == "plugins/Dummy_0.1"
|
assert r2.analysis[0].algorithm == "endpoint:plugins/Dummy_0.1"
|
||||||
assert r1.entries[0]['nif:isString'] == 'input'
|
assert r1.entries[0]['nif:isString'] == 'input'
|
||||||
|
|
||||||
def test_analyse_error(self):
|
def test_analyse_error(self):
|
||||||
mm = mock.MagicMock()
|
class ErrorPlugin(plugins.Analysis):
|
||||||
mm.id = 'magic_mock'
|
author = 'nobody'
|
||||||
mm.name = 'mock'
|
version = 0
|
||||||
mm.is_activated = True
|
ex = Error()
|
||||||
mm.analyse_entries.side_effect = Error('error in analysis', status=500)
|
|
||||||
self.senpy.add_plugin(mm)
|
def process(self, *args, **kwargs):
|
||||||
|
raise self.ex
|
||||||
|
|
||||||
|
m = ErrorPlugin(ex=Error('error in analysis', status=500))
|
||||||
|
self.senpy.add_plugin(m)
|
||||||
try:
|
try:
|
||||||
analyse(self.senpy, input='nothing', algorithm='MOCK')
|
analyse(self.senpy, input='nothing', algorithm='ErrorPlugin')
|
||||||
assert False
|
assert False
|
||||||
except Error as ex:
|
except Error as ex:
|
||||||
assert 'error in analysis' in ex['message']
|
assert 'error in analysis' in ex['message']
|
||||||
assert ex['status'] == 500
|
assert ex['status'] == 500
|
||||||
|
|
||||||
ex = Exception('generic exception on analysis')
|
m.ex = Exception('generic exception on analysis')
|
||||||
mm.analyse.side_effect = ex
|
|
||||||
mm.analyse_entries.side_effect = ex
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
analyse(self.senpy, input='nothing', algorithm='MOCK')
|
analyse(self.senpy, input='nothing', algorithm='ErrorPlugin')
|
||||||
assert False
|
assert False
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
assert 'generic exception on analysis' in ex['message']
|
assert 'generic exception on analysis' in str(ex)
|
||||||
assert ex['status'] == 500
|
|
||||||
|
|
||||||
def test_filtering(self):
|
def test_filtering(self):
|
||||||
""" Filtering plugins """
|
""" Filtering plugins """
|
||||||
@@ -196,7 +198,7 @@ class ExtensionsTest(TestCase):
|
|||||||
|
|
||||||
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.examples_dir, default_plugins=True)
|
||||||
assert len(senpy.plugins()) > 1
|
assert len(senpy.plugins(is_activated=False)) > 1
|
||||||
|
|
||||||
def test_convert_emotions(self):
|
def test_convert_emotions(self):
|
||||||
self.senpy.activate_all(sync=True)
|
self.senpy.activate_all(sync=True)
|
||||||
@@ -212,27 +214,28 @@ class ExtensionsTest(TestCase):
|
|||||||
'emoml:valence': 0
|
'emoml:valence': 0
|
||||||
}))
|
}))
|
||||||
response = Results({
|
response = Results({
|
||||||
'analysis': [{'plugin': plugin}],
|
'analysis': [plugin],
|
||||||
'entries': [Entry({
|
'entries': [Entry({
|
||||||
'nif:isString': 'much ado about nothing',
|
'nif:isString': 'much ado about nothing',
|
||||||
'emotions': [eSet1]
|
'emotions': [eSet1]
|
||||||
})]
|
})]
|
||||||
})
|
})
|
||||||
params = {'emotionModel': 'emoml:big6',
|
params = {'emotionModel': 'emoml:big6',
|
||||||
|
'algorithm': ['conversion'],
|
||||||
'conversion': 'full'}
|
'conversion': 'full'}
|
||||||
r1 = deepcopy(response)
|
r1 = deepcopy(response)
|
||||||
r1.parameters = params
|
r1.parameters = params
|
||||||
self.senpy.convert_emotions(r1)
|
self.senpy.analyse(r1)
|
||||||
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
|
r2.parameters = params
|
||||||
self.senpy.convert_emotions(r2)
|
self.senpy.analyse(r2)
|
||||||
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
|
r3.parameters = params
|
||||||
self.senpy.convert_emotions(r3)
|
self.senpy.analyse(r3)
|
||||||
assert len(r3.entries[0].emotions) == 1
|
assert len(r3.entries[0].emotions) == 1
|
||||||
r3.jsonld()
|
r3.jsonld()
|
||||||
|
@@ -5,10 +5,12 @@ import jsonschema
|
|||||||
import json
|
import json
|
||||||
import rdflib
|
import rdflib
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from senpy.models import (Emotion,
|
from senpy.models import (Analysis,
|
||||||
|
Emotion,
|
||||||
EmotionAnalysis,
|
EmotionAnalysis,
|
||||||
EmotionSet,
|
EmotionSet,
|
||||||
Entry,
|
Entry,
|
||||||
|
Entity,
|
||||||
Error,
|
Error,
|
||||||
Results,
|
Results,
|
||||||
Sentiment,
|
Sentiment,
|
||||||
@@ -60,7 +62,7 @@ class ModelsTest(TestCase):
|
|||||||
def test_id(self):
|
def test_id(self):
|
||||||
""" Adding the id after creation should overwrite the automatic ID
|
""" Adding the id after creation should overwrite the automatic ID
|
||||||
"""
|
"""
|
||||||
r = Entry()
|
r = Entry(_auto_id=True)
|
||||||
j = r.jsonld()
|
j = r.jsonld()
|
||||||
assert '@id' in j
|
assert '@id' in j
|
||||||
r.id = "test"
|
r.id = "test"
|
||||||
@@ -188,6 +190,19 @@ class ModelsTest(TestCase):
|
|||||||
assert isinstance(js['plugins'], list)
|
assert isinstance(js['plugins'], list)
|
||||||
assert js['plugins'][0]['@type'] == 'sentimentPlugin'
|
assert js['plugins'][0]['@type'] == 'sentimentPlugin'
|
||||||
|
|
||||||
|
def test_parameters(self):
|
||||||
|
'''An Analysis should contain the algorithm and the list of parameters to be used'''
|
||||||
|
a = Analysis()
|
||||||
|
a.params = {'param1': 1, 'param2': 2}
|
||||||
|
assert len(a.parameters) == 2
|
||||||
|
for param in a.parameters:
|
||||||
|
if param.name == 'param1':
|
||||||
|
assert param.value == 1
|
||||||
|
elif param.name == 'param2':
|
||||||
|
assert param.value == 2
|
||||||
|
else:
|
||||||
|
raise Exception('Unknown value %s' % param)
|
||||||
|
|
||||||
def test_from_string(self):
|
def test_from_string(self):
|
||||||
results = {
|
results = {
|
||||||
'@type': 'results',
|
'@type': 'results',
|
||||||
@@ -207,3 +222,14 @@ class ModelsTest(TestCase):
|
|||||||
recovered = from_string(string)
|
recovered = from_string(string)
|
||||||
assert isinstance(recovered, Results)
|
assert isinstance(recovered, Results)
|
||||||
assert isinstance(recovered.entries[0], Entry)
|
assert isinstance(recovered.entries[0], Entry)
|
||||||
|
|
||||||
|
def test_serializable(self):
|
||||||
|
r = Results()
|
||||||
|
e = Entry()
|
||||||
|
ent = Entity()
|
||||||
|
e.entities.append(ent)
|
||||||
|
r.entries.append(e)
|
||||||
|
d = r.serializable()
|
||||||
|
assert d
|
||||||
|
assert d['entries']
|
||||||
|
assert d['entries'][0]['entities']
|
||||||
|
@@ -5,10 +5,13 @@ import pickle
|
|||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase, skipIf
|
||||||
from senpy.models import Results, Entry, EmotionSet, Emotion, Plugins
|
from senpy.models import Results, Entry, EmotionSet, Emotion, Plugins
|
||||||
from senpy import plugins
|
from senpy import plugins
|
||||||
from senpy.plugins.conversion.emotion.centroids import CentroidConversion
|
from senpy.plugins.postprocessing.emotion.centroids import CentroidConversion
|
||||||
|
from senpy.gsitk_compat import GSITK_AVAILABLE
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
class ShelfDummyPlugin(plugins.SentimentPlugin, plugins.ShelfMixin):
|
class ShelfDummyPlugin(plugins.SentimentPlugin, plugins.ShelfMixin):
|
||||||
@@ -212,7 +215,7 @@ class PluginsTest(TestCase):
|
|||||||
def input(self, entry, **kwargs):
|
def input(self, entry, **kwargs):
|
||||||
return entry.text
|
return entry.text
|
||||||
|
|
||||||
def predict(self, input):
|
def predict_one(self, input):
|
||||||
return 'SIGN' in input
|
return 'SIGN' in input
|
||||||
|
|
||||||
def output(self, output, entry, **kwargs):
|
def output(self, output, entry, **kwargs):
|
||||||
@@ -242,7 +245,7 @@ class PluginsTest(TestCase):
|
|||||||
|
|
||||||
mappings = {'happy': 'marl:Positive', 'sad': 'marl:Negative'}
|
mappings = {'happy': 'marl:Positive', 'sad': 'marl:Negative'}
|
||||||
|
|
||||||
def predict(self, input, **kwargs):
|
def predict_one(self, input, **kwargs):
|
||||||
return 'happy' if ':)' in input else 'sad'
|
return 'happy' if ':)' in input else 'sad'
|
||||||
|
|
||||||
test_cases = [
|
test_cases = [
|
||||||
@@ -309,6 +312,50 @@ class PluginsTest(TestCase):
|
|||||||
res = c._backwards_conversion(e)
|
res = c._backwards_conversion(e)
|
||||||
assert res["onyx:hasEmotionCategory"] == "c2"
|
assert res["onyx:hasEmotionCategory"] == "c2"
|
||||||
|
|
||||||
|
def _test_evaluation(self):
|
||||||
|
testdata = []
|
||||||
|
for i in range(50):
|
||||||
|
testdata.append(["good", 1])
|
||||||
|
for i in range(50):
|
||||||
|
testdata.append(["bad", 0])
|
||||||
|
dataset = pd.DataFrame(testdata, columns=['text', 'polarity'])
|
||||||
|
|
||||||
|
class DummyPlugin(plugins.TextBox):
|
||||||
|
description = 'Plugin to test evaluation'
|
||||||
|
version = 0
|
||||||
|
|
||||||
|
def predict_one(self, input):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
class SmartPlugin(plugins.TextBox):
|
||||||
|
description = 'Plugin to test evaluation'
|
||||||
|
version = 0
|
||||||
|
|
||||||
|
def predict_one(self, input):
|
||||||
|
if input == 'good':
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
dpipe = DummyPlugin()
|
||||||
|
results = plugins.evaluate(datasets={'testdata': dataset}, plugins=[dpipe], flatten=True)
|
||||||
|
dumb_metrics = results[0].metrics[0]
|
||||||
|
assert abs(dumb_metrics['accuracy'] - 0.5) < 0.01
|
||||||
|
|
||||||
|
spipe = SmartPlugin()
|
||||||
|
results = plugins.evaluate(datasets={'testdata': dataset}, plugins=[spipe], flatten=True)
|
||||||
|
smart_metrics = results[0].metrics[0]
|
||||||
|
assert abs(smart_metrics['accuracy'] - 1) < 0.01
|
||||||
|
|
||||||
|
@skipIf(not GSITK_AVAILABLE, "GSITK is not available")
|
||||||
|
def test_evaluation(self):
|
||||||
|
self._test_evaluation()
|
||||||
|
|
||||||
|
@skipIf(GSITK_AVAILABLE, "GSITK is available")
|
||||||
|
def test_evaluation_unavailable(self):
|
||||||
|
with self.assertRaises(Exception) as context:
|
||||||
|
self._test_evaluation()
|
||||||
|
self.assertTrue('GSITK ' in str(context.exception))
|
||||||
|
|
||||||
|
|
||||||
def make_mini_test(fpath):
|
def make_mini_test(fpath):
|
||||||
def mini_test(self):
|
def mini_test(self):
|
||||||
|
@@ -8,6 +8,8 @@ from fnmatch import fnmatch
|
|||||||
|
|
||||||
from jsonschema import RefResolver, Draft4Validator, ValidationError
|
from jsonschema import RefResolver, Draft4Validator, ValidationError
|
||||||
|
|
||||||
|
from senpy.models import read_schema
|
||||||
|
|
||||||
root_path = path.join(path.dirname(path.realpath(__file__)), '..')
|
root_path = path.join(path.dirname(path.realpath(__file__)), '..')
|
||||||
schema_folder = path.join(root_path, 'senpy', 'schemas')
|
schema_folder = path.join(root_path, 'senpy', 'schemas')
|
||||||
examples_path = path.join(root_path, 'docs', 'examples')
|
examples_path = path.join(root_path, 'docs', 'examples')
|
||||||
@@ -15,7 +17,8 @@ bad_examples_path = path.join(root_path, 'docs', 'bad-examples')
|
|||||||
|
|
||||||
|
|
||||||
class JSONSchemaTests(unittest.TestCase):
|
class JSONSchemaTests(unittest.TestCase):
|
||||||
pass
|
def test_definitions(self):
|
||||||
|
read_schema('definitions.json')
|
||||||
|
|
||||||
|
|
||||||
def do_create_(jsfile, success):
|
def do_create_(jsfile, success):
|
||||||
|
33
tests/test_test.py
Normal file
33
tests/test_test.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
from senpy.testing import patch_requests
|
||||||
|
from senpy.models import Results
|
||||||
|
|
||||||
|
ENDPOINT = 'http://example.com'
|
||||||
|
|
||||||
|
|
||||||
|
class TestTest(TestCase):
|
||||||
|
def test_patch_text(self):
|
||||||
|
with patch_requests(ENDPOINT, 'hello'):
|
||||||
|
r = requests.get(ENDPOINT)
|
||||||
|
assert r.text == 'hello'
|
||||||
|
|
||||||
|
def test_patch_json(self):
|
||||||
|
r = Results()
|
||||||
|
with patch_requests(ENDPOINT, r):
|
||||||
|
res = requests.get(ENDPOINT)
|
||||||
|
assert res.text == json.dumps(r.jsonld())
|
||||||
|
js = res.json()
|
||||||
|
assert js
|
||||||
|
assert js['@type'] == r['@type']
|
||||||
|
|
||||||
|
def test_patch_dict(self):
|
||||||
|
r = {'nothing': 'new'}
|
||||||
|
with patch_requests(ENDPOINT, r):
|
||||||
|
res = requests.get(ENDPOINT)
|
||||||
|
assert res.text == json.dumps(r)
|
||||||
|
js = res.json()
|
||||||
|
assert js
|
||||||
|
assert js['nothing'] == 'new'
|
Reference in New Issue
Block a user