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

Compare commits

..

46 Commits

Author SHA1 Message Date
J. Fernando Sánchez
87589e994a Fixed version.sh 2017-02-27 12:32:34 +01:00
J. Fernando Sánchez
9f6a6f5ecd Loads of changes!
* Added conversion plugins (API might change!)
* Added conversion to the analysis pipeline
* Changed behaviour of --default-plugins (it adds conversion plugins regardless)
* Added emotionModel [sic] and emotionConversion models

//TODO add conversion tests
//TODO add conversion to docs
2017-02-27 12:01:19 +01:00
J. Fernando Sánchez
3cea7534ef New versioning
Use git to automatically fetch the version
2017-02-17 16:21:44 +01:00
J. Fernando Sánchez
7eaf303124 Added coverage tests 2017-02-17 11:24:57 +01:00
J. Fernando Sánchez
b4ca5f4a7c Several fixes and changes
* Added interactive debugging
* Better exception logging
* More tests for errors
* Added ONBUILD to dockerfile
  Now creating new images based on senpy's is as easy as:
  ```from senpy:<version>```. This will automatically mount the code to
  /senpy-plugins and install all dependencies
* Added /data as a VOLUME
* Added `--use-wheel` to pip install both on the image and in the
  auto-install function.
* Closes #9

Break compatibilitity:

* Removed ability to (de)activate plugins through the web
2017-02-17 09:56:53 +01:00
J. Fernando Sánchez
3311af2167 Bumped to v0.7.1 2017-02-13 20:43:27 +01:00
J. Fernando Sánchez
a4694dff2c Merge branch 'gitlabci' 2017-02-13 20:42:04 +01:00
J. Fernando Sánchez
6cb669cdb1 Added docker auth to docker push job 2017-02-13 20:36:12 +01:00
J. Fernando Sánchez
506feec13d Fixed docker push 2017-02-13 20:24:10 +01:00
J. Fernando Sánchez
2e3a6b7c84 TAGNAME->SLUG and cache in .eggs 2017-02-13 20:07:20 +01:00
J. Fernando Sánchez
7cc8b562f4 Moved before_script to images 2017-02-13 19:43:52 +01:00
J. Fernando Sánchez
528bbcac35 Added gitlab-ci docker build jobs 2017-02-13 19:41:18 +01:00
J. Fernando Sánchez
068241fb72 CI_REGISTRY_NAME 2017-02-13 18:34:35 +01:00
J. Fernando Sánchez
39d86a2050 Configured runner to mount socket 2017-02-13 18:29:38 +01:00
J. Fernando Sánchez
5371c83ab0 speeding up testing of the CI pipeline 2017-02-13 17:35:00 +01:00
J. Fernando Sánchez
673992dbe8 Docker dind service made global 2017-02-13 17:16:38 +01:00
J. Fernando Sánchez
eb3a42c247 Updated gitlabci 2017-02-13 12:30:44 +01:00
J. Fernando Sánchez
20357d2a0d Added gitlab CI 2017-02-13 12:04:29 +01:00
J. Fernando Sánchez
e9d7980e42 Merge branch 'jsonplay' into 'master'
Jsonplay

Closes #8

See merge request !9
2017-02-09 14:03:19 +00:00
J. Fernando Sánchez
908090f634 Released v0.7
Bug-fixes and improvements:
* Closes #5
* Closes #1
* Adds Client (beta)
* Added several schemas
* Lighter string representation -> should avoid delays in the analysis
  with plugins that have 'heavy' attributes

Backwards-incompatible changes:
* Context in headers by default
* All schemas include a "@type" argument that is used for autodetection
  in the client

... And possibly many more, this is still <1.0
2017-02-08 21:55:17 +01:00
militarpancho
cb963dc438 Playground improved. This closes #8 2017-02-06 14:08:13 +01:00
militarpancho
477cb18db1 Added tabs to choose view for the response. #8 2017-02-03 14:33:14 +01:00
J. Fernando Sánchez
fbf0384985 Replaced gevent with threading
* Replaced gevent (testing)
* Trying the slim python image (1/3 of previous size)
2017-02-02 16:35:58 +01:00
militarpancho
7a2c016cc6 added jsoneditor javascript plugin in relation with issue #8 2017-02-02 14:31:37 +01:00
J. Fernando Sánchez
b072121e20 Added Model string representation
This should help with performance issues with models that have large
private variables.
2017-02-02 05:01:40 +01:00
J. Fernando Sánchez
ceed9b97d0 Entries should be a set instead of lists
This allows for better framing when two entries have the same @id
2017-01-10 17:01:28 +01:00
J. Fernando Sánchez
2dbdb58b06 Fixed bug with sdist's name convention 2017-01-10 16:59:28 +01:00
J. Fernando Sánchez
db30257373 Flake8, Semver, Pre-commit
* Added pre-commit: http://pre-commit.com
* Fixed flake8 errors
* Added flake8 pre-commit hooks
* Added pre-commit to Makefile
* Changed VERSION numbering
* Changed versioning to match PEP-0440
2017-01-10 16:25:01 +01:00
J. Fernando Sánchez
7fd69cc690 YAPFed 2017-01-10 10:19:32 +01:00
J. Fernando Sánchez
b543a4614e Improved schema validation
* Added debug Dockerfile/Makefile
* Validation of examples in docs
2017-01-10 10:02:14 +01:00
J. Fernando Sánchez
bc1f9e4cf5 Split definitions into individual files 2016-12-26 18:49:53 +01:00
J. Fernando Sánchez
d72a995fa9 New shelf location and better shelf tests 2016-12-26 17:45:17 +01:00
J. Fernando Sánchez
40b67503ce Updated links in README 2016-12-19 13:17:56 +01:00
J. Fernando Sánchez
8624562f02 Dockerfiles not ignored anymore 2016-12-14 17:06:50 +01:00
J. Fernando Sánchez
4dee623ef9 Better makefile 2016-12-14 14:38:58 +01:00
J. Fernando Sánchez
2e7530d9bc Bumped to 0.6.1 2016-09-21 20:29:16 +02:00
J. Fernando Sánchez
07b5dd3823 Added option to install dependencies in CLI 2016-09-21 20:27:30 +02:00
J. Fernando Sánchez
0d511ad3c3 Bumped to 0.6.0
* Downloads pip requirements
* Modified Makefile
2016-09-21 19:00:20 +02:00
J. Fernando Sánchez
7205a0e7b2 Moved to gsiupm
* Updated sphinx docs to include schemas and version
* Added docker push to makefile
2016-09-21 10:10:49 +02:00
J. Fernando Sánchez
fff38bf825 Added 3.4 to travis 2016-09-20 20:58:37 +02:00
J. Fernando Sánchez
5d5de0bc50 Makefile for automated testing (no more drone) 2016-09-20 20:55:59 +02:00
J. Fernando Sánchez
0454fb1afe Updated installation instructions 2016-07-13 16:08:02 +02:00
J. Fernando Sánchez
5e36c71fa7 Merge branch 'master' of github.com:gsi-upm/senpy 2016-05-06 11:54:32 +02:00
J. Fernando Sánchez
c8e742f96e Bumped 0.5.6 2016-05-06 11:50:46 +02:00
J. Fernando Sánchez
1e7ae13700 Substituted json with yaml 2016-05-06 11:46:20 +02:00
NachoCP
bf30c04a52 Updated readthedocs 2016-03-31 11:56:32 +02:00
99 changed files with 40128 additions and 969 deletions

3
.gitignore vendored
View File

@@ -4,4 +4,5 @@
dist dist
build build
README.html README.html
__pycache__ __pycache__
VERSION

80
.gitlab-ci.yml Normal file
View File

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

View File

@@ -1,6 +1,8 @@
language: python language: python
python: python:
- "2.7" - "2.7"
- "3.4"
- "3.5"
install: "pip install -r requirements.txt" install: "pip install -r requirements.txt"
# run nosetests - Tests # run nosetests - Tests
script: nosetests script: nosetests

View File

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

1
Dockerfile Symbolic link
View File

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

21
Dockerfile-2.7 Normal file
View File

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

21
Dockerfile-3.4 Normal file
View File

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

21
Dockerfile-3.5 Normal file
View File

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

33
Dockerfile.deps Normal file
View File

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

21
Dockerfile.template Normal file
View File

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

View File

@@ -1,9 +1,9 @@
include requirements.txt include requirements.txt
include test-requirements.txt include test-requirements.txt
include README.md include README.rst
include senpy/context.jsonld include senpy/VERSION
graft senpy/plugins graft senpy/plugins
graft senpy/schemas graft senpy/schemas
graft senpy/templates graft senpy/templates
graft senpy/static graft senpy/static
graft img graft img

102
Makefile Normal file
View File

@@ -0,0 +1,102 @@
PYVERSIONS=3.5 3.4 2.7
PYMAIN=$(firstword $(PYVERSIONS))
NAME=senpy
REPO=gsiupm
VERSION=$(shell ./version.sh)
TARNAME=$(NAME)-$(VERSION).tar.gz
IMAGENAME=$(REPO)/$(NAME):$(VERSION)
TEST_COMMAND=gitlab-runner exec docker --cache-dir=/tmp/gitlabrunner --docker-volumes /tmp/gitlabrunner:/tmp/gitlabrunner --env CI_PROJECT_NAME=$(NAME)
all: build run
FORCE:
version: FORCE
@echo $(VERSION) > $(NAME)/VERSION
@echo $(VERSION)
yapf:
yapf -i -r senpy
yapf -i -r tests
dev:
pip install --user pre-commit
pre-commit install
dockerfiles: $(addprefix Dockerfile-,$(PYVERSIONS))
@unlink Dockerfile >/dev/null
ln -s Dockerfile-$(PYMAIN) Dockerfile
Dockerfile-%: Dockerfile.template
sed "s/{{PYVERSION}}/$*/" Dockerfile.template > Dockerfile-$*
quick_build: $(addprefix build-, $(PYMAIN))
build: $(addprefix build-, $(PYVERSIONS))
build-%: Dockerfile-%
docker build -t '$(IMAGENAME)-python$*' -f Dockerfile-$* .;
quick_test: $(addprefix test-,$(PYMAIN))
test: $(addprefix test-,$(PYVERSIONS))
debug-%:
@docker start $(NAME)-debug || (\
$(MAKE) build-$*; \
docker run -d -w /usr/src/app/ -v $$PWD:/usr/src/app --entrypoint=/bin/bash -p 5000:5000 -ti --name $(NAME)-debug '$(IMAGENAME)-python$*'; \
docker exec -ti $(NAME)-debug pip install -r test-requirements.txt; \
)\
docker attach $(NAME)-debug
debug: debug-$(PYMAIN)
test-%:
$(TEST_COMMAND) test-$*
dist/$(TARNAME):
docker run --rm -ti -v $$PWD:/usr/src/app/ -w /usr/src/app/ python:$(PYMAIN) python setup.py sdist;
sdist: dist/$(TARNAME)
pip_test-%: sdist
docker run --rm -v $$PWD/dist:/dist/ -ti python:$* pip install /dist/$(TARNAME);
pip_test: $(addprefix pip_test-,$(PYVERSIONS))
upload-%: test-%
docker push '$(IMAGENAME)-python$*'
upload: test $(addprefix upload-,$(PYVERSIONS))
docker tag '$(IMAGENAME)-python$(PYMAIN)' '$(IMAGENAME)'
docker tag '$(IMAGENAME)-python$(PYMAIN)' '$(REPO)/$(NAME)'
docker push '$(IMAGENAME)'
docker push '$(REPO)/$(NAME)'
clean:
@docker ps -a | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1;}}' | xargs docker rm -v 2>/dev/null|| true
@docker images | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1":"$$2;}}' | xargs docker rmi 2>/dev/null|| true
@docker rmi $(NAME)-debug 2>/dev/null || true
git_commit:
git commit -a
git_tag:
git tag ${VERSION}
upload_git:
git push --tags origin master
pip_upload:
python setup.py sdist upload ;
pip_test: $(addprefix pip_test-,$(PYVERSIONS))
run-%: build-%
docker run --rm -p 5000:5000 -ti '$(IMAGENAME)-python$(PYMAIN)' --default-plugins
run: run-$(PYMAIN)
.PHONY: test test-% build-% build test pip_test run yapf dev

View File

@@ -12,7 +12,7 @@ Have you ever wanted to turn your sentiment analysis algorithms into a service?
With senpy, now you can. With senpy, now you can.
It provides all the tools so you just have to worry about improving your algorithms: It provides all the tools so you just have to worry about improving your algorithms:
`See it in action. <http://demos.gsi.dit.upm.es/senpy>`_ `See it in action. <http://senpy.cluster.gsi.dit.upm.es/>`_
Installation Installation
------------ ------------
@@ -30,7 +30,7 @@ Alternatively, you can use the development version:
.. code:: bash .. code:: bash
git clone git@github.com:gsi-upm/senpy git clone http://github.com/gsi-upm/senpy
cd senpy cd senpy
pip install --user . pip install --user .
@@ -38,9 +38,9 @@ If you want to install senpy globally, use sudo instead of the ``--user`` flag.
Docker Image Docker Image
************ ************
Build the image or use the pre-built one: ``docker run -ti -p 5000:5000 balkian/senpy --host 0.0.0.0 --default-plugins``. Build the image or use the pre-built one: ``docker run -ti -p 5000:5000 gsiupm/senpy --host 0.0.0.0 --default-plugins``.
To add custom plugins, add a volume and tell senpy where to find the plugins: ``docker run -ti -p 5000:5000 -v <PATH OF PLUGINS>:/plugins balkian/senpy --host 0.0.0.0 --default-plugins -f /plugins`` To add custom plugins, add a volume and tell senpy where to find the plugins: ``docker run -ti -p 5000:5000 -v <PATH OF PLUGINS>:/plugins gsiupm/senpy --host 0.0.0.0 --default-plugins -f /plugins``
Usage Usage
----- -----

43
app.py
View File

@@ -1,43 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright 2014 J. Fernando Sánchez Rada - Grupo de Sistemas Inteligentes
# DIT, UPM
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This is a helper for development. If you want to run Senpy use:
python -m senpy
"""
from gevent.monkey import patch_all; patch_all()
import gevent
import config
from flask import Flask
from senpy.extensions import Senpy
import logging
import os
from gevent.wsgi import WSGIServer
logging.basicConfig(level=logging.DEBUG)
app = Flask(__name__)
mypath = os.path.dirname(os.path.realpath(__file__))
sp = Senpy(app, os.path.join(mypath, "plugins"), default_plugins=True)
sp.activate_all()
if __name__ == '__main__':
import logging
logging.basicConfig(level=config.DEBUG)
app.debug = config.DEBUG
http_server = WSGIServer(('', config.SERVER_PORT), app)
http_server.serve_forever()

1
docs/_static/schemas vendored Symbolic link
View File

@@ -0,0 +1 @@
../../senpy/schemas/

View File

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

View File

@@ -0,0 +1,18 @@
{
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
"@id": "http://example.com#NIFExample",
"@type": "results",
"analysis": [
],
"entries": [
{
"@type": [
"nif:RFC5147String",
"nif:Context"
],
"nif:beginIndex": 0,
"nif:endIndex": 40,
"nif:isString": "My favourite actress is Natalie Portman"
}
]
}

View File

@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# flake8: noqa
# #
# Senpy documentation build configuration file, created by # Senpy documentation build configuration file, created by
# sphinx-quickstart on Tue Feb 24 08:57:32 2015. # sphinx-quickstart on Tue Feb 24 08:57:32 2015.
@@ -52,16 +53,17 @@ master_doc = 'index'
# General information about the project. # General information about the project.
project = u'Senpy' project = u'Senpy'
copyright = u'2015, J. Fernando Sánchez' copyright = u'2016, J. Fernando Sánchez'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '0.4' with open('../senpy/VERSION') as f:
version = f.read().strip()
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '0.4' release = version
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.

View File

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

View File

@@ -1,6 +1,7 @@
{ {
"@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@context": "http://mixedemotions-project.eu/ns/context.jsonld",
"@id": "http://example.com#NIFExample", "@id": "http://example.com#NIFExample",
"@type": "results",
"analysis": [ "analysis": [
], ],
"entries": [ "entries": [

View File

@@ -1,6 +1,7 @@
{ {
"@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@context": "http://mixedemotions-project.eu/ns/context.jsonld",
"@id": "me:Result1", "@id": "me:Result1",
"@type": "results",
"analysis": [ "analysis": [
{ {
"@id": "me:SAnalysis1", "@id": "me:SAnalysis1",
@@ -52,7 +53,8 @@
"@id": "http://micro.blog/status1#char=16,77", "@id": "http://micro.blog/status1#char=16,77",
"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"
} }
], ],
"sentiments": [ "sentiments": [

View File

@@ -1,6 +1,7 @@
{ {
"@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@context": "http://mixedemotions-project.eu/ns/context.jsonld",
"@id": "me:Result1", "@id": "me:Result1",
"@type": "results",
"analysis": [ "analysis": [
{ {
"@id": "me:EmotionAnalysis1", "@id": "me:EmotionAnalysis1",

View File

@@ -1,6 +1,7 @@
{ {
"@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@context": "http://mixedemotions-project.eu/ns/context.jsonld",
"@id": "me:Result1", "@id": "me:Result1",
"@type": "results",
"analysis": [ "analysis": [
{ {
"@id": "me:NER1", "@id": "me:NER1",

View File

@@ -0,0 +1,46 @@
{
"@context": [
"http://mixedemotions-project.eu/ns/context.jsonld",
{
"emovoc": "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/emotionml/ns#"
}
],
"@id": "me:Result1",
"@type": "results",
"analysis": [
{
"@id": "me:HesamsAnalysis",
"@type": "onyx:EmotionAnalysis",
"onyx:usesEmotionModel": "emovoc:pad-dimensions"
}
],
"entries": [
{
"@id": "Entry1",
"@type": [
"nif:RFC5147String",
"nif:Context"
],
"nif:isString": "This is a test string",
"entities": [
],
"suggestions": [
],
"sentiments": [
],
"emotions": [
{
"@id": "Entry1#char=0,21",
"nif:anchorOf": "This is a test string",
"prov:wasGeneratedBy": "me:HesamAnalysis",
"onyx:hasEmotion": [
{
"emovoc:pleasure": 0.5,
"emovoc:arousal": 0.7
}
]
}
]
}
]
}

View File

@@ -1,6 +1,7 @@
{ {
"@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@context": "http://mixedemotions-project.eu/ns/context.jsonld",
"@id": "me:Result1", "@id": "me:Result1",
"@type": "results",
"analysis": [ "analysis": [
{ {
"@id": "me:SAnalysis1", "@id": "me:SAnalysis1",

View File

@@ -1,6 +1,7 @@
{ {
"@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@context": "http://mixedemotions-project.eu/ns/context.jsonld",
"@id": "me:Result1", "@id": "me:Result1",
"@type": "results",
"analysis": [ "analysis": [
{ {
"@id": "me:SgAnalysis1", "@id": "me:SgAnalysis1",
@@ -23,7 +24,8 @@
"@id": "http://micro.blog/status1#char=16,77", "@id": "http://micro.blog/status1#char=16,77",
"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"
} }
], ],
"sentiments": [ "sentiments": [

View File

@@ -22,6 +22,6 @@ If you want to install senpy globally, use sudo instead of the ``--user`` flag.
Docker Image Docker Image
************ ************
Build the image or use the pre-built one: ``docker run -ti -p 5000:5000 balkian/senpy --host 0.0.0.0 --default-plugins``. Build the image or use the pre-built one: ``docker run -ti -p 5000:5000 gsiupm/senpy --host 0.0.0.0 --default-plugins``.
To add custom plugins, add a volume and tell senpy where to find the plugins: ``docker run -ti -p 5000:5000 -v <PATH OF PLUGINS>:/plugins balkian/senpy --host 0.0.0.0 --default-plugins -f /plugins`` To add custom plugins, add a volume and tell senpy where to find the plugins: ``docker run -ti -p 5000:5000 -v <PATH OF PLUGINS>:/plugins gsiupm/senpy --host 0.0.0.0 --default-plugins -f /plugins``

View File

@@ -5,41 +5,124 @@ Each plugin represents a different analysis process.There are two types of files
- Definition file, has the ".senpy" extension. - Definition file, has the ".senpy" extension.
- Code file, is a python file. - Code file, is a python file.
This separation will allow us to deploy plugins that use the same code but employ different parameters.
For instance, one could use the same classifier and processing in several plugins, but train with different datasets.
This scenario is particularly useful for evaluation purposes.
The only limitation is that the name of each plugin needs to be unique.
Plugins Definitions Plugins Definitions
=================== ===================
The definition file can be written in JSON or YAML, where the data representation consists on attribute-value pairs. The definition file contains all the attributes of the plugin, and can be written in YAML or JSON.
The principal attributes are: The most important attributes are:
* name: plugin name used in senpy to call the plugin. * **name**: unique name that senpy will use internally to identify the plugin.
* module: indicates the module that will be loaded * **module**: indicates the module that contains the plugin code, which will be automatically loaded by senpy.
* **version**
* extra_params: used to specify parameters that the plugin accepts that are not already part of the senpy API. Those parameters may be required, and have aliased names. For instance:
.. code:: python .. code:: yaml
extra_params:
hello_param:
aliases: # required
- hello_param
- hello
required: true
default: Hi you
values:
- Hi you
- Hello y'all
- Howdy
Parameter validation will fail if a required parameter without a default has not been provided, or if the definition includes a set of values and the provided one does not match one of them.
A complete example:
.. code:: yaml
name: <Name of the plugin>
module: <Python file>
version: 0.1
And the json equivalent:
.. code:: json
{ {
"name" : "senpyPlugin", "name": "<Name of the plugin>",
"module" : "{python code file}" "module": "<Python file>",
"version": "0.1"
} }
.. code:: python
name: senpyPlugin
module: {python code file}
Plugins Code Plugins Code
================= ============
The basic methods in a plugin are: The basic methods in a plugin are:
* __init__ * __init__
* activate: used to load memory-hungry resources * activate: used to load memory-hungry resources
* deactivate: used to free up resources * deactivate: used to free up resources
* analyse: called in every user requests. It takes in the parameters supplied by a user and should return a senpy Results. * analyse_entry: called in every user requests. It takes in the parameters supplied by a user and should yield one or more ``Entry`` objects.
Plugins are loaded asynchronously, so don't worry if the activate method takes too long. The plugin will be marked as activated once it is finished executing the method. Plugins are loaded asynchronously, so don't worry if the activate method takes too long. The plugin will be marked as activated once it is finished executing the method.
Example plugin
==============
In this section, we will implement a basic sentiment analysis plugin.
To determine the polarity of each entry, the plugin will compare the length of the string to a threshold.
This threshold will be included in the definition file.
The definition file would look like this:
.. code:: yaml
name: helloworld
module: helloworld
version: 0.0
threshold: 10
Now, in a file named ``helloworld.py``:
.. code:: python
#!/bin/env python
#helloworld.py
from senpy.plugins import SenpyPlugin
from senpy.models import Sentiment
class HelloWorld(SenpyPlugin):
def analyse_entry(entry, params):
'''Basically do nothing with each entry'''
sentiment = Sentiment()
if len(entry.text) < self.threshold:
sentiment['marl:hasPolarity'] = 'marl:Positive'
else:
sentiment['marl:hasPolarity'] = 'marl:Negative'
entry.sentiments.append(sentiment)
yield entry
F.A.Q. F.A.Q.
====== ======
Why does the analyse function yield instead of return?
??????????????????????????????????????????????????????
This is so that plugins may add new entries to the response or filter some of them.
For instance, a `context detection` plugin may add a new entry for each context in the original entry.
On the other hand, a conveersion plugin may leave out those entries that do not contain relevant information.
If I'm using a classifier, where should I train it? If I'm using a classifier, where should I train it?
??????????????????????????????????????????????????? ???????????????????????????????????????????????????
@@ -76,17 +159,17 @@ This example ilustrate how to implement the Sentiment140 service as a plugin in
.. code:: python .. code:: python
class Sentiment140Plugin(SentimentPlugin): class Sentiment140Plugin(SentimentPlugin):
def analyse(self, **params): def analyse_entry(self, entry, params):
text = entry.text
lang = params.get("language", "auto") lang = params.get("language", "auto")
res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson", res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson",
json.dumps({"language": lang, json.dumps({"language": lang,
"data": [{"text": params["input"]}] "data": [{"text": text}]
} }
) )
) )
p = params.get("prefix", None) p = params.get("prefix", None)
response = Results(prefix=p)
polarity_value = self.maxPolarityValue*int(res.json()["data"][0] polarity_value = self.maxPolarityValue*int(res.json()["data"][0]
["polarity"]) * 0.25 ["polarity"]) * 0.25
polarity = "marl:Neutral" polarity = "marl:Neutral"
@@ -96,18 +179,13 @@ This example ilustrate how to implement the Sentiment140 service as a plugin in
elif polarity_value < neutral_value: elif polarity_value < neutral_value:
polarity = "marl:Negative" polarity = "marl:Negative"
entry = Entry(id="Entry0",
nif__isString=params["input"])
sentiment = Sentiment(id="Sentiment0", sentiment = Sentiment(id="Sentiment0",
prefix=p, prefix=p,
marl__hasPolarity=polarity, marl__hasPolarity=polarity,
marl__polarityValue=polarity_value) marl__polarityValue=polarity_value)
sentiment.prov__wasGeneratedBy = self.id sentiment.prov__wasGeneratedBy = self.id
entry.sentiments = []
entry.sentiments.append(sentiment) entry.sentiments.append(sentiment)
entry.language = lang yield entry
response.entries.append(entry)
return response
Where can I define extra parameters to be introduced in the request to my plugin? Where can I define extra parameters to be introduced in the request to my plugin?
@@ -141,9 +219,9 @@ The extraction of this paremeter is used in the analyse method of the Plugin int
Where can I set up variables for using them in my plugin? Where can I set up variables for using them in my plugin?
????????????????????????????????????????????????????????? ?????????????????????????????????????????????????????????
You can add these variables in the definition file with the extracture of attribute-value pair. You can add these variables in the definition file with the structure of attribute-value pairs.
Once you have added your variables, the next step is to extract them into the plugin. The plugin's __init__ method has a parameter called `info` where you can extract the values of the variables. This info parameter has the structure of a python dictionary. Every field added to the definition file is available to the plugin instance.
Can I activate a DEBUG mode for my plugin? Can I activate a DEBUG mode for my plugin?
??????????????????????????????????????????? ???????????????????????????????????????????
@@ -152,7 +230,15 @@ You can activate the DEBUG mode by the command-line tool using the option -d.
.. code:: bash .. code:: bash
python -m senpy -d senpy -d
Additionally, with the ``--pdb`` option you will be dropped into a pdb post mortem shell if an exception is raised.
.. code:: bash
senpy --pdb
Where can I find more code examples? Where can I find more code examples?
???????????????????????????????????? ????????????????????????????????????

View File

@@ -1,6 +1,6 @@
Schema Examples Schema Examples
=============== ===============
All the examples in this page use the schema defined in :ref:`schema`. All the examples in this page use the :download:`the main schema <_static/schemas/definitions.json>`.
Simple NIF annotation Simple NIF annotation
--------------------- ---------------------

View File

@@ -1,11 +1,11 @@
Flask>=0.10.1 Flask>=0.10.1
gunicorn>=19.0.0
requests>=2.4.1 requests>=2.4.1
GitPython>=0.3.2.RC1
gevent>=1.1rc4 gevent>=1.1rc4
PyLD>=0.6.5 PyLD>=0.6.5
Flask-Testing>=0.4.2
six six
future future
jsonschema jsonschema
jsonref jsonref
PyYAML
rdflib
rdflib-jsonld

View File

@@ -17,5 +17,13 @@
""" """
Sentiment analysis server in Python Sentiment analysis server in Python
""" """
from __future__ import print_function
from .version import __version__
__version__ = "0.5.5" import logging
logger = logging.getLogger(__name__)
logger.info('Using senpy version: {}'.format(__version__))
__all__ = ['api', 'blueprints', 'cli', 'extensions', 'models', 'plugins']

View File

@@ -24,9 +24,9 @@ from flask import Flask
from senpy.extensions import Senpy from senpy.extensions import Senpy
from gevent.wsgi import WSGIServer from gevent.wsgi import WSGIServer
from gevent.monkey import patch_all from gevent.monkey import patch_all
import gevent
import logging import logging
import os import os
import sys
import argparse import argparse
import senpy import senpy
@@ -34,44 +34,78 @@ patch_all(thread=False)
SERVER_PORT = os.environ.get("PORT", 5000) SERVER_PORT = os.environ.get("PORT", 5000)
def info(type, value, tb):
if hasattr(sys, 'ps1') or not sys.stderr.isatty():
# we are in interactive mode or we don't have a tty-like
# device, so we call the default hook
sys.__excepthook__(type, value, tb)
else:
import traceback
import pdb
# we are NOT in interactive mode, print the exception...
traceback.print_exception(type, value, tb)
print
# ...then start the debugger in post-mortem mode.
# pdb.pm() # deprecated
pdb.post_mortem(tb) # more "modern"
def main(): def main():
parser = argparse.ArgumentParser(description='Run a Senpy server') parser = argparse.ArgumentParser(description='Run a Senpy server')
parser.add_argument('--level', parser.add_argument(
'-l', '--level',
metavar='logging_level', '-l',
type=str, metavar='logging_level',
default="INFO", type=str,
help='Logging level') default="INFO",
parser.add_argument('--debug', help='Logging level')
'-d', parser.add_argument(
action='store_true', '--debug',
default=False, '-d',
help='Run the application in debug mode') action='store_true',
parser.add_argument('--default-plugins', default=False,
action='store_true', help='Run the application in debug mode')
default=False, parser.add_argument(
help='Load the default plugins') '--default-plugins',
parser.add_argument('--host', action='store_true',
type=str, default=False,
default="127.0.0.1", help='Load the default plugins')
help='Use 0.0.0.0 to accept requests from any host.') parser.add_argument(
parser.add_argument('--port', '--host',
'-p', type=str,
type=int, default="0.0.0.0",
default=SERVER_PORT, help='Use 0.0.0.0 to accept requests from any host.')
help='Port to listen on.') parser.add_argument(
parser.add_argument('--plugins-folder', '--port',
'-f', '-p',
type=str, type=int,
default='plugins', default=SERVER_PORT,
help='Where to look for plugins.') help='Port to listen on.')
parser.add_argument(
'--plugins-folder',
'-f',
type=str,
default='plugins',
help='Where to look for plugins.')
parser.add_argument(
'--only-install',
'-i',
action='store_true',
default=False,
help='Do not run a server, only install plugin dependencies')
args = parser.parse_args() args = parser.parse_args()
logging.basicConfig() logging.basicConfig()
rl = logging.getLogger() rl = logging.getLogger()
rl.setLevel(getattr(logging, args.level)) rl.setLevel(getattr(logging, args.level))
app = Flask(__name__) app = Flask(__name__)
app.debug = args.debug app.debug = args.debug
if args.debug:
sys.excepthook = info
sp = Senpy(app, args.plugins_folder, default_plugins=args.default_plugins) sp = Senpy(app, args.plugins_folder, default_plugins=args.default_plugins)
if args.only_install:
sp.install_deps()
return
sp.activate_all() sp.activate_all()
http_server = WSGIServer((args.host, args.port), app) http_server = WSGIServer((args.host, args.port), app)
try: try:
@@ -80,8 +114,10 @@ def main():
args.port)) args.port))
http_server.serve_forever() http_server.serve_forever()
except KeyboardInterrupt: except KeyboardInterrupt:
http_server.stop()
print('Bye!') print('Bye!')
http_server.stop()
sp.deactivate_all()
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@@ -1,13 +1,37 @@
from future.utils import iteritems from future.utils import iteritems
from .models import Error
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from .models import Error
API_PARAMS = { API_PARAMS = {
"algorithm": { "algorithm": {
"aliases": ["algorithm", "a", "algo"], "aliases": ["algorithm", "a", "algo"],
"required": False, "required": False,
},
"outformat": {
"@id": "outformat",
"aliases": ["outformat", "o"],
"default": "json-ld",
"required": True,
"options": ["json-ld", "turtle"],
},
"expanded-jsonld": {
"@id": "expanded-jsonld",
"aliases": ["expanded", "expanded-jsonld"],
"required": True,
"default": 0
},
"emotionModel": {
"@id": "emotionModel",
"aliases": ["emotionModel", "emoModel"],
"required": False
},
"conversion": {
"@id": "conversion",
"description": "How to show the elements that have (not) been converted",
"required": True,
"options": ["filtered", "nested", "full"],
"default": "full"
} }
} }
@@ -25,7 +49,7 @@ CLI_PARAMS = {
"required": True, "required": True,
"default": "." "default": "."
}, },
} }
NIF_PARAMS = { NIF_PARAMS = {
"input": { "input": {
@@ -48,13 +72,6 @@ NIF_PARAMS = {
"default": "direct", "default": "direct",
"options": ["direct", "url", "file"], "options": ["direct", "url", "file"],
}, },
"outformat": {
"@id": "outformat",
"aliases": ["outformat", "o"],
"default": "json-ld",
"required": False,
"options": ["json-ld"],
},
"language": { "language": {
"@id": "language", "@id": "language",
"aliases": ["language", "l"], "aliases": ["language", "l"],
@@ -77,12 +94,12 @@ NIF_PARAMS = {
def parse_params(indict, spec=NIF_PARAMS): def parse_params(indict, spec=NIF_PARAMS):
outdict = {} logger.debug("Parsing: {}\n{}".format(indict, spec))
outdict = indict.copy()
wrong_params = {} wrong_params = {}
for param, options in iteritems(spec): for param, options in iteritems(spec):
if param[0] != "@": # Exclude json-ld properties if param[0] != "@": # Exclude json-ld properties
logger.debug("Param: %s - Options: %s", param, options) for alias in options.get("aliases", []):
for alias in options["aliases"]:
if alias in indict: if alias in indict:
outdict[param] = indict[alias] outdict[param] = indict[alias]
if param not in outdict: if param not in outdict:
@@ -96,10 +113,12 @@ def parse_params(indict, spec=NIF_PARAMS):
outdict[param] not in spec[param]["options"]: outdict[param] not in spec[param]["options"]:
wrong_params[param] = spec[param] wrong_params[param] = spec[param]
if wrong_params: if wrong_params:
message = Error(status=404, logger.debug("Error parsing: %s", wrong_params)
message="Missing or invalid parameters", message = Error(
parameters=outdict, status=400,
errors={param: error for param, error in message="Missing or invalid parameters",
iteritems(wrong_params)}) parameters=outdict,
errors={param: error
for param, error in iteritems(wrong_params)})
raise message raise message
return outdict return outdict

View File

@@ -17,18 +17,21 @@
""" """
Blueprints for Senpy Blueprints for Senpy
""" """
from flask import Blueprint, request, current_app, render_template, url_for, jsonify from flask import (Blueprint, request, current_app, render_template, url_for,
jsonify)
from .models import Error, Response, Plugins, read_schema from .models import Error, Response, Plugins, read_schema
from .api import NIF_PARAMS, WEB_PARAMS, parse_params from .api import WEB_PARAMS, API_PARAMS, parse_params
from .version import __version__
from functools import wraps from functools import wraps
import json
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
api_blueprint = Blueprint("api", __name__) api_blueprint = Blueprint("api", __name__)
demo_blueprint = Blueprint("demo", __name__) demo_blueprint = Blueprint("demo", __name__)
ns_blueprint = Blueprint("ns", __name__)
def get_params(req): def get_params(req):
if req.method == 'POST': if req.method == 'POST':
@@ -42,43 +45,71 @@ def get_params(req):
@demo_blueprint.route('/') @demo_blueprint.route('/')
def index(): def index():
return render_template("index.html") return render_template("index.html", version=__version__)
@api_blueprint.route('/contexts/<entity>.jsonld') @api_blueprint.route('/contexts/<entity>.jsonld')
def context(entity="context"): def context(entity="context"):
return jsonify({"@context": Response.context}) context = Response._context
context['@vocab'] = url_for('ns.index', _external=True)
return jsonify({"@context": context})
@ns_blueprint.route('/') # noqa: F811
def index():
context = Response._context
context['@vocab'] = url_for('.ns', _external=True)
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 jsonify(read_schema(schema))
except Exception: # Should be FileNotFoundError, but it's missing from py2 except Exception: # Should be FileNotFoundError, but it's missing from py2
return Error(message="Schema not found", status=404).flask() return Error(message="Schema not found", status=404).flask()
def basic_api(f): def basic_api(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
print('Getting request:')
print(request)
raw_params = get_params(request) raw_params = get_params(request)
web_params = parse_params(raw_params, spec=WEB_PARAMS) headers = {'X-ORIGINAL-PARAMS': raw_params}
# Get defaults
web_params = parse_params({}, spec=WEB_PARAMS)
api_params = parse_params({}, spec=API_PARAMS)
if hasattr(request, 'params'): outformat = 'json-ld'
request.params.update(raw_params)
else:
request.params = raw_params
try: try:
print('Getting request:')
print(request)
web_params = parse_params(raw_params, spec=WEB_PARAMS)
api_params = parse_params(raw_params, spec=API_PARAMS)
if hasattr(request, 'params'):
request.params.update(api_params)
else:
request.params = api_params
response = f(*args, **kwargs) response = f(*args, **kwargs)
except Error as ex: except Error as ex:
response = ex response = ex
in_headers = web_params["inHeaders"] != "0"
headers = {'X-ORIGINAL-PARAMS': raw_params} in_headers = web_params['inHeaders'] != "0"
return response.flask(in_headers=in_headers, expanded = api_params['expanded-jsonld']
headers=headers, outformat = api_params['outformat']
context_uri=url_for('api.context', entity=type(response).__name__,
_external=True)) return response.flask(
in_headers=in_headers,
headers=headers,
prefix=url_for('.api', _external=True),
context_uri=url_for('api.context',
entity=type(response).__name__,
_external=True),
outformat=outformat,
expanded=expanded)
return decorated_function return decorated_function
@api_blueprint.route('/', methods=['POST', 'GET']) @api_blueprint.route('/', methods=['POST', 'GET'])
@basic_api @basic_api
def api(): def api():
@@ -92,33 +123,18 @@ def plugins():
sp = current_app.senpy sp = current_app.senpy
dic = Plugins(plugins=list(sp.plugins.values())) dic = Plugins(plugins=list(sp.plugins.values()))
return dic return dic
@api_blueprint.route('/plugins/<plugin>/', methods=['POST', 'GET']) @api_blueprint.route('/plugins/<plugin>/', methods=['POST', 'GET'])
@api_blueprint.route('/plugins/<plugin>/<action>', methods=['POST', 'GET'])
@basic_api @basic_api
def plugin(plugin=None, action="list"): def plugin(plugin=None):
filt = {}
sp = current_app.senpy sp = current_app.senpy
plugs = sp.filter_plugins(name=plugin)
if plugin == 'default' and sp.default_plugin: if plugin == 'default' and sp.default_plugin:
response = sp.default_plugin return sp.default_plugin
plugin = response.name plugins = sp.filter_plugins(
elif plugin in sp.plugins: id='plugins/{}'.format(plugin)) or sp.filter_plugins(name=plugin)
response = sp.plugins[plugin] if plugins:
response = list(plugins.values())[0]
else: else:
return Error(message="Plugin not found", status=404) return Error(message="Plugin not found", status=404)
if action == "list": return response
return response
method = "{}_plugin".format(action)
if(hasattr(sp, method)):
getattr(sp, method)(plugin)
return Response(message="Ok")
else:
return Error(message="action '{}' not allowed".format(action))
if __name__ == '__main__':
import config
app.register_blueprint(api_blueprint)
app.debug = config.DEBUG
app.run(host='0.0.0.0', port=5000)

View File

@@ -3,6 +3,7 @@ from .models import Error
from .api import parse_params, CLI_PARAMS from .api import parse_params, CLI_PARAMS
from .extensions import Senpy from .extensions import Senpy
def argv_to_dict(argv): def argv_to_dict(argv):
'''Turns parameters in the form of '--key value' into a dict {'key': 'value'} '''Turns parameters in the form of '--key value' into a dict {'key': 'value'}
''' '''
@@ -11,13 +12,14 @@ def argv_to_dict(argv):
for i in range(len(argv)): for i in range(len(argv)):
if argv[i][0] == '-': if argv[i][0] == '-':
key = argv[i].strip('-') key = argv[i].strip('-')
value = argv[i+1] if len(argv)>i+1 else None value = argv[i + 1] if len(argv) > i + 1 else None
if value and value[0] == '-': if value and value[0] == '-':
cli_dict[key] = "" cli_dict[key] = ""
else: else:
cli_dict[key] = value cli_dict[key] = value
return cli_dict return cli_dict
def parse_cli(argv): def parse_cli(argv):
cli_dict = argv_to_dict(argv) cli_dict = argv_to_dict(argv)
cli_params = parse_params(cli_dict, spec=CLI_PARAMS) cli_params = parse_params(cli_dict, spec=CLI_PARAMS)
@@ -34,6 +36,7 @@ def main_function(argv):
res = sp.analyse(**cli_dict) res = sp.analyse(**cli_dict)
return res return res
def main(): def main():
'''This method is the entrypoint for the CLI (as configured un setup.py) '''This method is the entrypoint for the CLI (as configured un setup.py)
''' '''
@@ -43,8 +46,7 @@ def main():
except Error as err: except Error as err:
print(err.to_JSON()) print(err.to_JSON())
sys.exit(2) sys.exit(2)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

35
senpy/client.py Normal file
View File

@@ -0,0 +1,35 @@
import requests
import logging
from . import models
logger = logging.getLogger(__name__)
class Client(object):
def __init__(self, endpoint):
self.endpoint = endpoint
def analyse(self, input, method='GET', **kwargs):
return self.request('/', method=method, input=input, **kwargs)
def request(self, path=None, method='GET', **params):
url = '{}{}'.format(self.endpoint, path)
response = requests.request(method=method, url=url, params=params)
try:
resp = models.from_dict(response.json())
resp.validate(resp)
return resp
except Exception as ex:
logger.error(('There seems to be a problem with the response:\n'
'\tURL: {url}\n'
'\tError: {error}\n'
'\t\n'
'#### Response:\n'
'\tCode: {code}'
'\tContent: {content}'
'\n').format(
error=ex,
url=url,
code=response.status_code,
content=response.content))
raise ex

View File

@@ -1,18 +1,16 @@
""" """
Main class for Senpy.
It orchestrates plugin (de)activation and analysis.
""" """
from future import standard_library from future import standard_library
standard_library.install_aliases() standard_library.install_aliases()
import gevent
from gevent import monkey
monkey.patch_all()
from .plugins import SenpyPlugin, SentimentPlugin, EmotionPlugin from .plugins import SentimentPlugin, SenpyPlugin
from .models import Error from .models import Error, Entry, Results
from .blueprints import api_blueprint, demo_blueprint from .blueprints import api_blueprint, demo_blueprint, ns_blueprint
from .api import API_PARAMS, NIF_PARAMS, parse_params from .api import API_PARAMS, NIF_PARAMS, parse_params
from git import Repo, InvalidGitRepositoryError from threading import Thread
from functools import partial
import os import os
import fnmatch import fnmatch
@@ -21,27 +19,32 @@ import sys
import imp import imp
import logging import logging
import traceback import traceback
import gevent import yaml
import json import pip
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Senpy(object): class Senpy(object):
""" Default Senpy extension for Flask """ """ Default Senpy extension for Flask """
def __init__(self, app=None, plugin_folder="plugins", default_plugins=False): def __init__(self,
app=None,
plugin_folder=".",
default_plugins=False):
self.app = app self.app = app
self._search_folders = set() self._search_folders = set()
self._plugin_list = [] self._plugin_list = []
self._outdated = True self._outdated = True
self._default = None
self.add_folder(plugin_folder) self.add_folder(plugin_folder)
if default_plugins: if default_plugins:
base_folder = os.path.join(os.path.dirname(__file__), "plugins") self.add_folder('plugins', from_root=True)
self.add_folder(base_folder) else:
# Add only conversion plugins
self.add_folder(os.path.join('plugins', 'conversion'),
from_root=True)
if app is not None: if app is not None:
self.init_app(app) self.init_app(app)
@@ -60,9 +63,12 @@ class Senpy(object):
else: else:
app.teardown_request(self.teardown) app.teardown_request(self.teardown)
app.register_blueprint(api_blueprint, url_prefix="/api") app.register_blueprint(api_blueprint, url_prefix="/api")
app.register_blueprint(ns_blueprint, url_prefix="/ns")
app.register_blueprint(demo_blueprint, url_prefix="/") app.register_blueprint(demo_blueprint, url_prefix="/")
def add_folder(self, folder): def add_folder(self, folder, from_root=False):
if from_root:
folder = os.path.join(os.path.dirname(__file__), folder)
logger.debug("Adding folder: %s", folder) logger.debug("Adding folder: %s", folder)
if os.path.isdir(folder): if os.path.isdir(folder):
self._search_folders.add(folder) self._search_folders.add(folder)
@@ -70,58 +76,140 @@ class Senpy(object):
else: else:
logger.debug("Not a folder: %s", folder) logger.debug("Not a folder: %s", folder)
def analyse(self, **params): def _find_plugin(self, params):
algo = None
logger.debug("analysing with params: {}".format(params))
api_params = parse_params(params, spec=API_PARAMS) api_params = parse_params(params, spec=API_PARAMS)
algo = None
if "algorithm" in api_params and api_params["algorithm"]: if "algorithm" in api_params and api_params["algorithm"]:
algo = api_params["algorithm"] algo = api_params["algorithm"]
elif self.plugins: elif self.plugins:
algo = self.default_plugin and self.default_plugin.name algo = self.default_plugin and self.default_plugin.name
if not algo: if not algo:
raise Error(status=404, raise Error(
message=("No plugins found." status=404,
" Please install one.").format(algo)) message=("No plugins found."
" Please install one.").format(algo))
if algo not in self.plugins: if algo not in self.plugins:
logger.debug(("The algorithm '{}' is not valid\n" logger.debug(("The algorithm '{}' is not valid\n"
"Valid algorithms: {}").format(algo, "Valid algorithms: {}").format(algo,
self.plugins.keys())) self.plugins.keys()))
raise Error(status=404, raise Error(
message="The algorithm '{}' is not valid" status=404,
.format(algo)) message="The algorithm '{}' is not valid".format(algo))
if not self.plugins[algo].is_activated: if not self.plugins[algo].is_activated:
logger.debug("Plugin not activated: {}".format(algo)) logger.debug("Plugin not activated: {}".format(algo))
raise Error(status=400, raise Error(
message=("The algorithm '{}'" status=400,
" is not activated yet").format(algo)) message=("The algorithm '{}'"
plug = self.plugins[algo] " is not activated yet").format(algo))
return self.plugins[algo]
def _get_params(self, params, plugin):
nif_params = parse_params(params, spec=NIF_PARAMS) nif_params = parse_params(params, spec=NIF_PARAMS)
extra_params = plug.get('extra_params', {}) extra_params = plugin.get('extra_params', {})
specific_params = parse_params(params, spec=extra_params) specific_params = parse_params(params, spec=extra_params)
nif_params.update(specific_params) nif_params.update(specific_params)
return nif_params
def _get_entries(self, params):
entry = None
if params['informat'] == 'text':
entry = Entry(text=params['input'])
else:
raise NotImplemented('Only text input format implemented')
yield entry
def analyse(self, **api_params):
logger.debug("analysing with params: {}".format(api_params))
plugin = self._find_plugin(api_params)
nif_params = self._get_params(api_params, plugin)
resp = Results()
if 'with_parameters' in api_params:
resp.parameters = nif_params
try: try:
resp = plug.analyse(**nif_params) entries = []
resp.analysis.append(plug) for i in self._get_entries(nif_params):
entries += list(plugin.analyse_entry(i, nif_params))
resp.entries = entries
self.convert_emotions(resp, plugin, nif_params)
resp.analysis.append(plugin.id)
logger.debug("Returning analysis result: {}".format(resp)) logger.debug("Returning analysis result: {}".format(resp))
except Error as ex:
logger.exception('Error returning analysis result')
resp = ex
except Exception as ex: except Exception as ex:
logger.exception('Error returning analysis result')
resp = Error(message=str(ex), status=500) resp = Error(message=str(ex), status=500)
return resp return resp
def _conversion_candidates(self, fromModel, toModel):
candidates = self.filter_plugins(**{'@type': 'emotionConversionPlugin'})
for name, candidate in candidates.items():
for pair in candidate.onyx__doesConversion:
logging.debug(pair)
if pair['onyx:conversionFrom'] == fromModel \
and pair['onyx:conversionTo'] == toModel:
# logging.debug('Found candidate: {}'.format(candidate))
yield candidate
def convert_emotions(self, resp, plugin, params):
"""
Conversion of all emotions in a response.
In addition to converting from one model to another, it has
to include the conversion plugin to the analysis list.
Needless to say, this is far from an elegant solution, but it works.
@todo refactor and clean up
"""
fromModel = plugin.get('onyx:usesEmotionModel', None)
toModel = params.get('emotionModel', None)
output = params.get('conversion', None)
logger.debug('Asked for model: {}'.format(toModel))
logger.debug('Analysis plugin uses model: {}'.format(fromModel))
if not toModel:
return
try:
candidate = next(self._conversion_candidates(fromModel, toModel))
except StopIteration:
e = Error(('No conversion plugin found for: '
'{} -> {}'.format(fromModel, toModel)))
e.original_response = resp
e.parameters = params
raise e
newentries = []
for i in resp.entries:
if output == "full":
newemotions = i.emotions.copy()
else:
newemotions = []
for j in i.emotions:
for k in candidate.convert(j, fromModel, toModel, params):
k.prov__wasGeneratedBy = candidate.id
if output == 'nested':
k.prov__wasDerivedFrom = j
newemotions.append(k)
i.emotions = newemotions
newentries.append(i)
resp.entries = newentries
resp.analysis.append(candidate.id)
@property @property
def default_plugin(self): def default_plugin(self):
candidates = self.filter_plugins(is_activated=True) candidate = self._default
if len(candidates) > 0: if not candidate:
candidate = list(candidates.values())[0] candidates = self.filter_plugins(is_activated=True)
logger.debug("Default: {}".format(candidate.name)) if len(candidates) > 0:
return candidate candidate = list(candidates.values())[0]
else: logger.debug("Default: {}".format(candidate))
return None return candidate
def parameters(self, algo): @default_plugin.setter
return getattr(self.plugins.get(algo) or self.default_plugin, def default_plugin(self, value):
"extra_params", if isinstance(value, SenpyPlugin):
{}) self._default = value
else:
self._default = self.plugins[value]
def activate_all(self, sync=False): def activate_all(self, sync=False):
ps = [] ps = []
@@ -145,103 +233,116 @@ class Senpy(object):
try: try:
plugin = self.plugins[plugin_name] plugin = self.plugins[plugin_name]
except KeyError: except KeyError:
raise Error(message="Plugin not found: {}".format(plugin_name), raise Error(
status=404) message="Plugin not found: {}".format(plugin_name), status=404)
logger.info("Activating plugin: {}".format(plugin.name)) logger.info("Activating plugin: {}".format(plugin.name))
def act(): def act():
success = False
try: try:
plugin.activate() plugin.activate()
logger.info("Plugin activated: {}".format(plugin.name)) msg = "Plugin activated: {}".format(plugin.name)
logger.info(msg)
success = True
self._set_active_plugin(plugin_name, success)
except Exception as ex: except Exception as ex:
logger.error("Error activating plugin {}: {}".format(plugin.name, msg = "Error activating plugin {} - {} : \n\t{}".format(
ex)) plugin.name, ex, traceback.format_exc())
logger.error("Trace: {}".format(traceback.format_exc())) logger.error(msg)
th = gevent.spawn(act) raise Error(msg)
th.link_value(partial(self._set_active_plugin, plugin_name, True))
if sync: if sync:
th.join() act()
else: else:
return th th = Thread(target=act)
th.start()
def deactivate_plugin(self, plugin_name, sync=False): def deactivate_plugin(self, plugin_name, sync=False):
try: try:
plugin = self.plugins[plugin_name] plugin = self.plugins[plugin_name]
except KeyError: except KeyError:
raise Error(message="Plugin not found: {}".format(plugin_name), raise Error(
status=404) message="Plugin not found: {}".format(plugin_name), status=404)
self._set_active_plugin(plugin_name, False)
def deact(): def deact():
try: try:
plugin.deactivate() plugin.deactivate()
logger.info("Plugin deactivated: {}".format(plugin.name)) logger.info("Plugin deactivated: {}".format(plugin.name))
except Exception as ex: except Exception as ex:
logger.error("Error deactivating plugin {}: {}".format(plugin.name, logger.error(
ex)) "Error deactivating plugin {}: {}".format(plugin.name, ex))
logger.error("Trace: {}".format(traceback.format_exc())) logger.error("Trace: {}".format(traceback.format_exc()))
th = gevent.spawn(deact)
th.link_value(partial(self._set_active_plugin, plugin_name, False))
if sync: if sync:
th.join() deact()
else: else:
return th th = Thread(target=deact)
th.start()
def reload_plugin(self, name): @classmethod
logger.debug("Reloading {}".format(name)) def validate_info(cls, info):
plugin = self.plugins[name] return all(x in info for x in ('name', 'module', 'version'))
try:
del self.plugins[name]
nplug = self._load_plugin(plugin.module, plugin.path)
self.plugins[nplug.name] = nplug
except Exception as ex:
logger.error('Error reloading {}: {}'.format(name, ex))
self.plugins[name] = plugin
@staticmethod def install_deps(self):
def _load_plugin(root, filename): for i in self.plugins.values():
logger.debug("Loading plugin: {}".format(filename)) self._install_deps(i._info)
fpath = os.path.join(root, filename)
with open(fpath, 'r') as f: @classmethod
info = json.load(f) def _install_deps(cls, info=None):
logger.debug("Info: {}".format(info)) requirements = info.get('requirements', [])
sys.path.append(root) if requirements:
pip_args = []
pip_args.append('install')
pip_args.append('--use-wheel')
for req in requirements:
pip_args.append(req)
logger.info('Installing requirements: ' + str(requirements))
pip.main(pip_args)
@classmethod
def _load_plugin_from_info(cls, info, root):
if not cls.validate_info(info):
logger.warn('The module info is not valid.\n\t{}'.format(info))
return None, None
module = info["module"] module = info["module"]
name = info["name"] name = info["name"]
sys.path.append(root)
(fp, pathname, desc) = imp.find_module(module, [root, ]) (fp, pathname, desc) = imp.find_module(module, [root, ])
try: cls._install_deps(info)
tmp = imp.load_module(module, fp, pathname, desc) tmp = imp.load_module(module, fp, pathname, desc)
sys.path.remove(root) sys.path.remove(root)
candidate = None candidate = None
for _, obj in inspect.getmembers(tmp): for _, obj in inspect.getmembers(tmp):
if inspect.isclass(obj) and inspect.getmodule(obj) == tmp: if inspect.isclass(obj) and inspect.getmodule(obj) == tmp:
logger.debug(("Found plugin class:" logger.debug(("Found plugin class:"
" {}@{}").format(obj, inspect.getmodule(obj)) " {}@{}").format(obj, inspect.getmodule(obj)))
) candidate = obj
candidate = obj break
break if not candidate:
if not candidate: logger.debug("No valid plugin for: {}".format(module))
logger.debug("No valid plugin for: {}".format(filename)) return
return module = candidate(info=info)
module = candidate(info=info)
try:
repo_path = root
module._repo = Repo(repo_path)
except InvalidGitRepositoryError:
module._repo = None
except Exception as ex:
logger.error("Exception importing {}: {}".format(filename, ex))
logger.error("Trace: {}".format(traceback.format_exc()))
return None, None
return name, module return name, module
@classmethod
def _load_plugin(cls, root, filename):
fpath = os.path.join(root, filename)
logger.debug("Loading plugin: {}".format(fpath))
with open(fpath, 'r') as f:
info = yaml.load(f)
logger.debug("Info: {}".format(info))
return cls._load_plugin_from_info(info, root)
def _load_plugins(self): def _load_plugins(self):
plugins = {} plugins = {}
for search_folder in self._search_folders: for search_folder in self._search_folders:
for root, dirnames, filenames in os.walk(search_folder): for root, dirnames, filenames in os.walk(search_folder):
for filename in fnmatch.filter(filenames, '*.senpy'): for filename in fnmatch.filter(filenames, '*.senpy'):
name, plugin = self._load_plugin(root, filename) name, plugin = self._load_plugin(root, filename)
if plugin and name not in self._plugin_list: if plugin and name:
plugins[name] = plugin plugins[name] = plugin
self._outdated = False self._outdated = False
@@ -262,9 +363,8 @@ class Senpy(object):
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("matching {} with {}: {}".format(plug.name, logger.debug(
kwargs, "matching {} with {}: {}".format(plug.name, kwargs, res))
res))
return res return res
if not kwargs: if not kwargs:
@@ -274,5 +374,8 @@ class Senpy(object):
def sentiment_plugins(self): def sentiment_plugins(self):
""" Return only the sentiment plugins """ """ Return only the sentiment plugins """
return {p: plugin for p, plugin in self.plugins.items() if return {
isinstance(plugin, SentimentPlugin)} p: plugin
for p, plugin in self.plugins.items()
if isinstance(plugin, SentimentPlugin)
}

View File

@@ -1,9 +1,9 @@
''' '''
Senpy Models. Senpy Models.
This implementation should mirror the JSON schema definition. This implementation should mirror the JSON schema definition.
For compatibility with Py3 and for easier debugging, this new version drops introspection For compatibility with Py3 and for easier debugging, this new version drops
and adds all arguments to the models. introspection and adds all arguments to the models.
''' '''
from __future__ import print_function from __future__ import print_function
from six import string_types from six import string_types
@@ -12,34 +12,43 @@ import time
import copy import copy
import json import json
import os import os
import logging
import jsonref import jsonref
import jsonschema import jsonschema
from flask import Response as FlaskResponse from flask import Response as FlaskResponse
from pyld import jsonld
from rdflib import Graph
import logging
logger = logging.getLogger(__name__)
DEFINITIONS_FILE = 'definitions.json' DEFINITIONS_FILE = 'definitions.json'
CONTEXT_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'schemas', 'context.jsonld') CONTEXT_PATH = os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'schemas', 'context.jsonld')
def get_schema_path(schema_file, absolute=False): def get_schema_path(schema_file, absolute=False):
if absolute: if absolute:
return os.path.realpath(schema_file) return os.path.realpath(schema_file)
else: else:
return os.path.join(os.path.dirname(os.path.realpath(__file__)), 'schemas', schema_file) return os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'schemas',
schema_file)
def read_schema(schema_file, absolute=False): def read_schema(schema_file, absolute=False):
schema_path = get_schema_path(schema_file, absolute) schema_path = get_schema_path(schema_file, absolute)
schema_uri = 'file://{}'.format(schema_path) schema_uri = 'file://{}'.format(schema_path)
return jsonref.load(open(schema_path), base_uri=schema_uri) with open(schema_path) as f:
return jsonref.load(f, base_uri=schema_uri)
base_schema = read_schema(DEFINITIONS_FILE) base_schema = read_schema(DEFINITIONS_FILE)
logging.debug(base_schema)
class Context(dict): class Context(dict):
@staticmethod @staticmethod
def load(context): def load(context):
logging.debug('Loading context: {}'.format(context)) logging.debug('Loading context: {}'.format(context))
@@ -59,16 +68,19 @@ class Context(dict):
except IOError: except IOError:
return context return context
else: else:
raise AttributeError('Please, provide a valid context') raise AttributeError('Please, provide a valid context')
base_context = Context.load(CONTEXT_PATH) base_context = Context.load(CONTEXT_PATH)
class SenpyMixin(object): class SenpyMixin(object):
context = base_context["@context"] _context = base_context["@context"]
def flask(self, def flask(self,
in_headers=False, in_headers=True,
headers=None, headers=None,
outformat='json-ld',
**kwargs): **kwargs):
""" """
Return the values and error to be used in flask. Return the values and error to be used in flask.
@@ -76,66 +88,92 @@ class SenpyMixin(object):
contexts if the plugin adds more aliases. contexts if the plugin adds more aliases.
""" """
headers = headers or {} headers = headers or {}
kwargs["with_context"] = True kwargs["with_context"] = not in_headers
js = self.jsonld(**kwargs) content, mimetype = self.serialize(format=outformat,
if in_headers: with_mime=True,
url = js["@context"] **kwargs)
del js["@context"]
headers.update({
"Link": ('<%s>;'
'rel="http://www.w3.org/ns/json-ld#context";'
' type="application/ld+json"' % url)
})
return FlaskResponse(json.dumps(js, indent=2, sort_keys=True),
status=getattr(self, "status", 200),
headers=headers,
mimetype="application/json")
if outformat == 'json-ld' and in_headers:
headers.update({
"Link":
('<%s>;'
'rel="http://www.w3.org/ns/json-ld#context";'
' type="application/ld+json"' % kwargs.get('context_uri'))
})
return FlaskResponse(
response=content,
status=getattr(self, "status", 200),
headers=headers,
mimetype=mimetype)
def serialize(self, format='json-ld', with_mime=False, **kwargs):
js = self.jsonld(**kwargs)
if format == 'json-ld':
content = json.dumps(js, indent=2, sort_keys=True)
mimetype = "application/json"
elif format in ['turtle', ]:
logger.debug(js)
content = json.dumps(js, indent=2, sort_keys=True)
g = Graph().parse(
data=content,
format='json-ld',
base=kwargs.get('prefix'),
context=self._context)
logger.debug(
'Parsing with prefix: {}'.format(kwargs.get('prefix')))
content = g.serialize(format='turtle').decode('utf-8')
mimetype = 'text/{}'.format(format)
else:
raise Error('Unknown outformat: {}'.format(format))
if with_mime:
return content, mimetype
else:
return content
def serializable(self): def serializable(self):
def ser_or_down(item): def ser_or_down(item):
if hasattr(item, 'serializable'): if hasattr(item, 'serializable'):
return item.serializable() return item.serializable()
elif isinstance(item, dict): elif isinstance(item, dict):
temp = dict() temp = dict()
for kp in item: for kp in item:
vp = item[kp] vp = item[kp]
temp[kp] = ser_or_down(vp) temp[kp] = ser_or_down(vp)
return temp return temp
elif isinstance(item, list): elif isinstance(item, list):
return list(ser_or_down(i) for i in item) return list(ser_or_down(i) for i in item)
else: else:
return item return item
return ser_or_down(self._plain_dict()) return ser_or_down(self._plain_dict())
def jsonld(self,
def jsonld(self, with_context=True, context_uri=None): with_context=True,
context_uri=None,
prefix=None,
expanded=False):
ser = self.serializable() ser = self.serializable()
if with_context: result = jsonld.compact(
context = [] ser,
if context_uri: self._context,
context = context_uri options={
else: 'base': prefix,
context = self.context.copy() 'expandContext': self._context,
if hasattr(self, 'prefix'): 'senpy': prefix
# This sets @base for the document, which will be used in })
# all relative URIs will. For example, if a uri is "Example" and if context_uri:
# prefix =s "http://example.com", the absolute URI after expanding result['@context'] = context_uri
# with JSON-LD will be "http://example.com/Example" if expanded:
result = jsonld.expand(
prefix_context = {"@base": self.prefix} result, options={'base': prefix,
if isinstance(context, list): 'expandContext': self._context})
context.append(prefix_context) if not with_context:
else: del result['@context']
context = [context, prefix_context] return result
ser["@context"] = context
return ser
def to_JSON(self, *args, **kwargs): def to_JSON(self, *args, **kwargs):
js = json.dumps(self.jsonld(*args, **kwargs), indent=4, js = json.dumps(self.jsonld(*args, **kwargs), indent=4, sort_keys=True)
sort_keys=True)
return js return js
def validate(self, obj=None): def validate(self, obj=None):
@@ -144,35 +182,39 @@ class SenpyMixin(object):
if hasattr(obj, "jsonld"): if hasattr(obj, "jsonld"):
obj = obj.jsonld() obj = obj.jsonld()
jsonschema.validate(obj, self.schema) jsonschema.validate(obj, self.schema)
class SenpyModel(SenpyMixin, dict): def __str__(self):
return str(self.to_JSON())
class BaseModel(SenpyMixin, dict):
schema = base_schema schema = base_schema
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.id = kwargs.pop('id', '{}_{}'.format(type(self).__name__, if 'id' in kwargs:
time.time())) self.id = kwargs.pop('id')
elif kwargs.pop('_auto_id', True):
self.id = '_:{}_{}'.format(type(self).__name__, time.time())
temp = dict(*args, **kwargs) temp = dict(*args, **kwargs)
for obj in [
self.schema,
] + self.schema.get('allOf', []):
for k, v in obj.get('properties', {}).items():
if 'default' in v and k not in temp:
temp[k] = copy.deepcopy(v['default'])
for i in temp: for i in temp:
nk = self._get_key(i) nk = self._get_key(i)
if nk != i: if nk != i:
temp[nk] = temp[i] temp[nk] = temp[i]
del temp[i] del temp[i]
try:
reqs = self.schema.get('required', []) temp['@type'] = getattr(self, '@type')
for i in reqs: except AttributeError:
if i not in temp: logger.warn('Creating an instance of an unknown model')
prop = self.schema['properties'][i] super(BaseModel, self).__init__(temp)
if 'default' in prop:
temp[i] = copy.deepcopy(prop['default'])
if 'context' in temp:
context = temp['context']
del temp['context']
self.__dict__['context'] = Context.load(context)
super(SenpyModel, self).__init__(temp)
def _get_key(self, key): def _get_key(self, key):
key = key.replace("__", ":", 1) key = key.replace("__", ":", 1)
@@ -181,7 +223,6 @@ class SenpyModel(SenpyMixin, dict):
def __setitem__(self, key, value): def __setitem__(self, key, value):
dict.__setitem__(self, key, value) dict.__setitem__(self, key, value)
def __delitem__(self, key): def __delitem__(self, key):
dict.__delitem__(self, key) dict.__delitem__(self, key)
@@ -196,53 +237,105 @@ class SenpyModel(SenpyMixin, dict):
def __delattr__(self, key): def __delattr__(self, key):
self.__delitem__(self._get_key(key)) self.__delitem__(self._get_key(key))
def _plain_dict(self): def _plain_dict(self):
d = { k: v for (k,v) in self.items() if k[0] != "_"} d = {k: v for (k, v) in self.items() if k[0] != "_"}
d["@id"] = d.pop('id') if 'id' in d:
d["@id"] = d.pop('id')
return d return d
class Response(SenpyModel):
schema = read_schema('response.json')
class Results(SenpyModel): _subtypes = {}
schema = read_schema('results.json')
class Entry(SenpyModel):
schema = read_schema('entry.json')
class Sentiment(SenpyModel): def register(rsubclass, rtype=None):
schema = read_schema('sentiment.json') _subtypes[rtype or rsubclass.__name__] = rsubclass
class Analysis(SenpyModel):
schema = read_schema('analysis.json')
class EmotionSet(SenpyModel): def from_dict(indict):
schema = read_schema('emotionSet.json') target = indict.get('@type', None)
if target and target in _subtypes:
cls = _subtypes[target]
else:
cls = BaseModel
return cls(**indict)
class Emotion(SenpyModel):
schema = read_schema('emotion.json')
class Suggestion(SenpyModel): def from_json(injson):
schema = read_schema('suggestion.json') indict = json.loads(injson)
return from_dict(indict)
class PluginModel(SenpyModel):
schema = read_schema('plugin.json')
class Plugins(SenpyModel): def from_schema(name, schema_file=None, base_classes=None):
schema = read_schema('plugins.json') base_classes = base_classes or []
base_classes.append(BaseModel)
schema_file = schema_file or '{}.json'.format(name)
class_name = '{}{}'.format(name[0].upper(), name[1:])
newclass = type(class_name, tuple(base_classes), {})
setattr(newclass, '@type', name)
setattr(newclass, 'schema', read_schema(schema_file))
setattr(newclass, 'class_name', class_name)
register(newclass, name)
return newclass
class Error(SenpyMixin, BaseException ):
def __init__(self, message, status=500, params=None, errors=None, *args, **kwargs): def _add_from_schema(*args, **kwargs):
generatedClass = from_schema(*args, **kwargs)
globals()[generatedClass.__name__] = generatedClass
del generatedClass
for i in [
'analysis',
'emotion',
'emotionConversion',
'emotionConversionPlugin',
'emotionAnalysis',
'emotionModel',
'emotionPlugin',
'emotionSet',
'entry',
'plugin',
'plugins',
'response',
'results',
'sentiment',
'sentimentPlugin',
'suggestion',
]:
_add_from_schema(i)
_ErrorModel = from_schema('error')
class Error(SenpyMixin, BaseException):
def __init__(self, message, *args, **kwargs):
super(Error, self).__init__(self, message, message)
self._error = _ErrorModel(message=message, *args, **kwargs)
self.message = message self.message = message
self.status = status
self.params = params or {}
self.errors = errors or ""
def _plain_dict(self): def __getitem__(self, key):
return self.__dict__ return self._error[key]
def __str__(self): def __setitem__(self, key, value):
return str(self.jsonld()) self._error[key] = value
def __delitem__(self, key):
del self._error[key]
def __getattr__(self, key):
if key != '_error' and hasattr(self._error, key):
return getattr(self._error, key)
raise AttributeError(key)
def __setattr__(self, key, value):
if key != '_error':
return setattr(self._error, key, value)
else:
super(Error, self).__setattr__(key, value)
def __delattr__(self, key):
delattr(self._error, key)
register(Error, 'error')

View File

@@ -5,28 +5,47 @@ import inspect
import os.path import os.path
import pickle import pickle
import logging import logging
from .models import Response, PluginModel, Error import tempfile
import copy
from . import models
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SenpyPlugin(PluginModel):
class SenpyPlugin(models.Plugin):
def __init__(self, info=None): def __init__(self, info=None):
"""
Provides a canonical name for plugins and serves as base for other
kinds of plugins.
"""
if not info: if not info:
raise Error(message=("You need to provide configuration" raise models.Error(message=("You need to provide configuration"
"information for the plugin.")) "information for the plugin."))
logger.debug("Initialising {}".format(info)) logger.debug("Initialising {}".format(info))
super(SenpyPlugin, self).__init__(info) id = 'plugins/{}_{}'.format(info['name'], info['version'])
self.id = '{}_{}'.format(self.name, self.version) super(SenpyPlugin, self).__init__(id=id, **info)
self._info = info
self.is_activated = False self.is_activated = False
def get_folder(self): def get_folder(self):
return os.path.dirname(inspect.getfile(self.__class__)) return os.path.dirname(inspect.getfile(self.__class__))
def analyse(self, *args, **kwargs): def analyse(self, *args, **kwargs):
logger.debug("Analysing with: {} {}".format(self.name, self.version)) raise NotImplemented(
pass 'Your method 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['text']
params = copy.copy(parameters)
params['input'] = text
results = self.analyse(**params)
for i in results.entries:
yield i
def activate(self): def activate(self):
pass pass
@@ -34,30 +53,27 @@ class SenpyPlugin(PluginModel):
def deactivate(self): def deactivate(self):
pass pass
def __del__(self):
''' Destructor, to make sure all the resources are freed '''
self.deactivate()
class SentimentPlugin(SenpyPlugin):
class SentimentPlugin(models.SentimentPlugin, SenpyPlugin):
def __init__(self, info, *args, **kwargs): def __init__(self, info, *args, **kwargs):
super(SentimentPlugin, self).__init__(info, *args, **kwargs) super(SentimentPlugin, self).__init__(info, *args, **kwargs)
self.minPolarityValue = float(info.get("minPolarityValue", 0)) self.minPolarityValue = float(info.get("minPolarityValue", 0))
self.maxPolarityValue = float(info.get("maxPolarityValue", 1)) self.maxPolarityValue = float(info.get("maxPolarityValue", 1))
self["@type"] = "marl:SentimentAnalysis"
class EmotionPlugin(SenpyPlugin): class EmotionPlugin(models.EmotionPlugin, SenpyPlugin):
def __init__(self, info, *args, **kwargs): def __init__(self, info, *args, **kwargs):
resp = super(EmotionPlugin, self).__init__(info, *args, **kwargs) super(EmotionPlugin, self).__init__(info, *args, **kwargs)
self.minEmotionValue = float(info.get("minEmotionValue", 0)) self.minEmotionValue = float(info.get("minEmotionValue", -1))
self.maxEmotionValue = float(info.get("maxEmotionValue", 0)) self.maxEmotionValue = float(info.get("maxEmotionValue", 1))
self["@type"] = "onyx:EmotionAnalysis"
class EmotionConversionPlugin(models.EmotionConversionPlugin, SenpyPlugin):
def __init__(self, info, *args, **kwargs):
super(EmotionConversionPlugin, self).__init__(info, *args, **kwargs)
class ShelfMixin(object): class ShelfMixin(object):
@property @property
def sh(self): def sh(self):
if not hasattr(self, '_sh') or self._sh is None: if not hasattr(self, '_sh') or self._sh is None:
@@ -73,22 +89,15 @@ class ShelfMixin(object):
del self.__dict__['_sh'] del self.__dict__['_sh']
self.save() self.save()
def __del__(self):
self.save()
super(ShelfMixin, self).__del__()
@property @property
def shelf_file(self): def shelf_file(self):
if not hasattr(self, '_shelf_file') or not self._shelf_file: if 'shelf_file' not in self or not self['shelf_file']:
if hasattr(self, '_info') and 'shelf_file' in self._info: self.shelf_file = os.path.join(tempfile.gettempdir(),
self.__dict__['_shelf_file'] = self._info['shelf_file'] self.name + '.p')
else: return self['shelf_file']
self._shelf_file = os.path.join(self.get_folder(), self.name + '.p')
return self._shelf_file
def save(self): def save(self):
logger.debug('closing pickle') logger.debug('saving pickle')
if hasattr(self, '_sh') and self._sh is not None: if hasattr(self, '_sh') and self._sh is not None:
with open(self.shelf_file, 'wb') as f: with open(self.shelf_file, 'wb') as f:
pickle.dump(self._sh, f) pickle.dump(self._sh, f)
del(self.__dict__['_sh'])

View File

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

View File

@@ -0,0 +1,35 @@
---
name: Ekman2VAD
module: ekman2vad
description: Plugin to convert from Ekman to VAD
version: 0.1
onyx:doesConversion:
- onyx:conversionFrom: emoml:big6
onyx:conversionTo: emoml:fsre-dimensions
- onyx:conversionFrom: emoml:fsre-dimensions
onyx:conversionTo: wna:WNAModel
centroids:
emoml:big6anger:
A: 6.95
D: 5.1
V: 2.7
emoml:big6disgust:
A: 5.3
D: 8.05
V: 2.7
emoml:big6fear:
A: 6.5
D: 3.6
V: 3.2
emoml:big6happiness:
A: 7.22
D: 6.28
V: 8.6
emoml:big6sadness:
A: 5.21
D: 2.82
V: 2.21
aliases:
A: emoml:arousal
V: emoml:potency
D: emoml:dominance

View File

@@ -0,0 +1,18 @@
import random
from senpy.plugins import EmotionPlugin
from senpy.models import EmotionSet, Emotion
class RmoRandPlugin(EmotionPlugin):
def analyse_entry(self, entry, params):
category = "emoml:big6happiness"
number = max(-1, min(1, random.gauss(0, 0.5)))
if number > 0:
category = "emoml:big6anger"
emotionSet = EmotionSet()
emotion = Emotion({"onyx:hasEmotionCategory": category})
emotionSet.onyx__hasEmotion.append(emotion)
emotionSet.prov__wasGeneratedBy = self.id
entry.emotions.append(emotionSet)
yield entry

View File

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

View File

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

View File

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

View File

@@ -1,41 +0,0 @@
import json
import random
from senpy.plugins import SentimentPlugin
from senpy.models import Results, Sentiment, Entry
class Sentiment140Plugin(SentimentPlugin):
def analyse(self, **params):
lang = params.get("language", "auto")
response = Results()
polarity_value = max(-1, min(1, random.gauss(0.2, 0.2)))
polarity = "marl:Neutral"
if polarity_value > 0:
polarity = "marl:Positive"
elif polarity_value < 0:
polarity = "marl:Negative"
entry = Entry({"id":":Entry0",
"nif:isString": params["input"]})
sentiment = Sentiment({"id": ":Sentiment0",
"marl:hasPolarity": polarity,
"marl:polarityValue": polarity_value})
sentiment["prov:wasGeneratedBy"] = self.id
entry.sentiments = []
entry.sentiments.append(sentiment)
entry.language = lang
response.entries.append(entry)
return response

View File

@@ -1,18 +0,0 @@
{
"name": "rand",
"module": "rand",
"description": "What my plugin broadly does",
"author": "@balkian",
"version": "0.1",
"extra_params": {
"language": {
"@id": "lang_rand",
"aliases": ["language", "l"],
"required": false,
"options": ["es", "en", "auto"]
}
},
"requirements": {},
"marl:maxPolarityValue": "1",
"marl:minPolarityValue": "-1"
}

View File

@@ -0,0 +1,36 @@
import requests
import json
from senpy.plugins import SentimentPlugin
from senpy.models import Sentiment
class Sentiment140Plugin(SentimentPlugin):
def analyse_entry(self, entry, params):
lang = params.get("language", "auto")
res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson",
json.dumps({
"language": lang,
"data": [{
"text": entry.text
}]
}))
p = params.get("prefix", None)
polarity_value = self.maxPolarityValue * int(
res.json()["data"][0]["polarity"]) * 0.25
polarity = "marl:Neutral"
neutral_value = self.maxPolarityValue / 2.0
if polarity_value > neutral_value:
polarity = "marl:Positive"
elif polarity_value < neutral_value:
polarity = "marl:Negative"
sentiment = Sentiment(
prefix=p,
marl__hasPolarity=polarity,
marl__polarityValue=polarity_value)
sentiment.prov__wasGeneratedBy = self.id
entry.sentiments = []
entry.sentiments.append(sentiment)
entry.language = lang
yield entry

View File

@@ -0,0 +1,21 @@
---
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
requirements: {}
maxPolarityValue: 1
minPolarityValue: 0

View File

@@ -1,40 +0,0 @@
import requests
import json
from senpy.plugins import SentimentPlugin
from senpy.models import Results, Sentiment, Entry
class Sentiment140Plugin(SentimentPlugin):
def analyse(self, **params):
lang = params.get("language", "auto")
res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson",
json.dumps({"language": lang,
"data": [{"text": params["input"]}]
}
)
)
p = params.get("prefix", None)
response = Results(prefix=p)
polarity_value = self.maxPolarityValue*int(res.json()["data"][0]
["polarity"]) * 0.25
polarity = "marl:Neutral"
neutral_value = self.maxPolarityValue / 2.0
if polarity_value > neutral_value:
polarity = "marl:Positive"
elif polarity_value < neutral_value:
polarity = "marl:Negative"
entry = Entry(id="Entry0",
nif__isString=params["input"])
sentiment = Sentiment(id="Sentiment0",
prefix=p,
marl__hasPolarity=polarity,
marl__polarityValue=polarity_value)
sentiment.prov__wasGeneratedBy = self.id
entry.sentiments = []
entry.sentiments.append(sentiment)
entry.language = lang
response.entries.append(entry)
return response

View File

@@ -1,18 +0,0 @@
{
"name": "sentiment140",
"module": "sentiment140",
"description": "What my plugin broadly does",
"author": "@balkian",
"version": "0.1",
"extra_params": {
"language": {
"@id": "lang_sentiment140",
"aliases": ["language", "l"],
"required": false,
"options": ["es", "en", "auto"]
}
},
"requirements": {},
"maxPolarityValue": "1",
"minPolarityValue": "0"
}

View File

@@ -1,4 +1,15 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "definitions.json#/Analysis" "description": "Senpy analysis",
"type": "object",
"properties": {
"@id": {
"type": "string"
},
"@type": {
"type": "string",
"description": "Type of the analysis. e.g. marl:SentimentAnalysis"
}
},
"required": ["@id", "@type"]
} }

15
senpy/schemas/atom.json Normal file
View File

@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Base schema for all Senpy objects",
"type": "object",
"properties": {
"@id": {
"type": "string"
},
"@type": {
"type": "string",
"description": "Type of the atom. e.g., 'onyx:EmotionAnalysis', 'nif:Entry'"
}
},
"required": ["@id", "@type"]
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "JSON-LD Context",
"type": ["array", "string", "object"]
}

View File

@@ -6,30 +6,51 @@
"prov": "http://www.w3.org/ns/prov#", "prov": "http://www.w3.org/ns/prov#",
"nif": "http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#", "nif": "http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#",
"marl": "http://www.gsi.dit.upm.es/ontologies/marl/ns#", "marl": "http://www.gsi.dit.upm.es/ontologies/marl/ns#",
"onyx": "http://www.gsi.dit.upm.es/ontologies/onyx#", "onyx": "http://www.gsi.dit.upm.es/ontologies/onyx/ns#",
"wnaffect": "http://www.gsi.dit.upm.es/ontologies/wnaffect#", "wna": "http://www.gsi.dit.upm.es/ontologies/wnaffect/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#",
"topics": { "topics": {
"@id": "dc:subject" "@id": "dc:subject"
}, },
"entities": { "entities": {
"@id": "me:hasEntities" "@id": "me:hasEntities"
}, },
"suggestions": { "suggestions": {
"@id": "me:hasSuggestions" "@id": "me:hasSuggestions",
"@container": "@set"
}, },
"emotions": { "emotions": {
"@id": "onyx:hasEmotionSet" "@id": "onyx:hasEmotionSet",
"@container": "@set"
}, },
"sentiments": { "sentiments": {
"@id": "marl:hasOpinion" "@id": "marl:hasOpinion",
"@container": "@set"
}, },
"entries": { "entries": {
"@id": "prov:used" "@id": "prov:used",
"@container": "@set"
}, },
"analysis": { "analysis": {
"@id": "prov:wasGeneratedBy" "@id": "AnalysisInvolved",
"@type": "@id",
"@container": "@set"
},
"prov:wasGeneratedBy": {
"@type": "@id"
},
"onyx:usesEmotionModel": {
"@type": "@id"
},
"onyx:hasEmotionCategory": {
"@type": "@id"
},
"onyx:conversionFrom": {
"@type": "@id"
},
"onyx:conversionTo": {
"@type": "@id"
} }
} }
} }

View File

@@ -1,169 +1,45 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"Results": { "Results": {
"title": "Results", "$ref": "results.json"
"description": "The results of an analysis",
"type": "object",
"properties": {
"@context": {
"$ref": "#/Context"
},
"@id": {
"description": "ID of the analysis",
"type": "string"
},
"analysis": {
"type": "array",
"default": [],
"items": {
"$ref": "#/Analysis"
}
},
"entries": {
"type": "array",
"default": [],
"items": {
"$ref": "#/Entry"
}
}
},
"required": ["@id", "analysis", "entries"]
}, },
"Context": { "Context": {
"description": "JSON-LD Context", "$ref": "context.json"
"type": ["array", "string", "object"]
}, },
"Analysis": { "Analysis": {
"description": "Senpy analysis", "$ref": "analysis.json"
"type": "object",
"properties": {
"@id": {
"type": "string"
},
"@type": {
"type": "string",
"description": "Type of the analysis. e.g. marl:SentimentAnalysis"
}
},
"required": ["@id", "@type"]
}, },
"Entry": { "Entry": {
"properties": { "$ref": "entry.json"
"@id": {
"type": "string"
},
"@type": {
"enum": [["nif:RFC5147String", "nif:Context"]]
},
"nif:isString": {
"description": "String contained in this Context",
"type": "string"
},
"sentiments": {
"type": "array",
"items": {"$ref": "#/Sentiment" }
},
"emotions": {
"type": "array",
"items": {"$ref": "#/EmotionSet" }
},
"entities": {
"type": "array",
"items": {"$ref": "#/Entity" }
},
"topics": {
"type": "array",
"items": {"$ref": "#/Topic" }
},
"suggestions": {
"type": "array",
"items": {"$ref": "#/Suggestion" }
}
},
"required": ["@id", "nif:isString"]
}, },
"Sentiment": { "Sentiment": {
"properties": { "$ref": "sentiment.json"
"@id": {"type": "string"},
"nif:beginIndex": {"type": "integer"},
"nif:endIndex": {"type": "integer"},
"nif:anchorOf": {
"description": "Piece of context that contains the Sentiment",
"type": "string"
},
"marl:hasPolarity": {
"enum": ["marl:Positive", "marl:Negative", "marl:Neutral"]
},
"marl:polarityValue": {
"type": "number"
},
"prov:wasGeneratedBy": {
"type": "string",
"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"]
}, },
"EmotionSet": { "EmotionSet": {
"properties": { "$ref": "emotionSet.json"
"@id": {"type": "string"},
"nif:beginIndex": {"type": "integer"},
"nif:endIndex": {"type": "integer"},
"nif:anchorOf": {
"description": "Piece of context that contains the Sentiment",
"type": "string"
},
"onyx:hasEmotion": {
"type": "array",
"items": {
"$ref": "#/Emotion"
},
"default": []
},
"prov:wasGeneratedBy": {
"type": "string",
"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"]
}, },
"Emotion": { "Emotion": {
"type": "object" "$ref": "emotion.json"
},
"EmotionModel": {
"$ref": "emotionModel.json"
}, },
"Entity": { "Entity": {
"type": "object" "$ref": "entity.json"
}, },
"Topic": { "Topic": {
"type": "object" "$ref": "topic.json"
}, },
"Suggestion": { "Suggestion": {
"type": "object" "$ref": "suggestion.json"
}, },
"Plugins": { "Plugins": {
"properties": { "$ref": "plugin.json"
"plugins": {
"type": "array",
"items": {
"$ref": "#/Plugin"
}
}
}
}, },
"Plugin": { "Plugin": {
"type": "object", "$ref": "plugin.json"
"required": ["@id", "extra_params"],
"properties": {
"@id": {
"type": "string"
},
"extra_params": {
"type": "object",
"default": {}
}
}
}, },
"Response": { "Response": {
"type": "object" "$ref": "response.json"
} }
} }

View File

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

View File

@@ -1,4 +1,4 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "definitions.json#/Emotion" "type": "object"
} }

View File

@@ -0,0 +1,19 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Senpy Emotion analysis",
"type": "object",
"allOf": [
{"$ref": "analysis.json"},
{"properties":
{
"onyx:usesEmotionModel": {
"anyOf": [
{"type": "string"},
{"$ref": "emotionModel.json"}
]
}
},
"required": ["onyx:hasEmotionModel",
"@type"]
}]
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"properties": {
"onyx:conversionFrom": {
"$ref": "emotionModel.json"
},
"onyx:conversionTo": {
"$ref": "emotionModel.json"
}
},
"required": ["onyx:conversionFrom", "onyx:conversionTo"]
}

View File

@@ -0,0 +1,19 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"allOf": [
{
"$ref": "plugin.json"
},
{
"properties": {
"onyx:doesConversion": {
"type": "array",
"items": {
"$ref": "emotionConversion.json"
}
}
}
}
]
}

View File

@@ -0,0 +1,27 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"properties": {
"@id": {"type": "string"},
"nif:beginIndex": {"type": "integer"},
"nif:endIndex": {"type": "integer"},
"nif:anchorOf": {
"description": "Piece of context that contains the Sentiment",
"type": "string"
},
"onyx:hasDimension": {
"type": "array",
"items": {
"$ref": "dimensions.json"
},
"uniqueItems": true
},
"onyx:hasEmotionCategory": {
"type": "array",
"items": {
"$ref": "emotion.json"
},
"default": []
}
},
"required": ["@id", "onyx:hasEmotion"]
}

View File

@@ -0,0 +1,19 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"allOf": [
{
"$ref": "plugin.json"
},
{
"properties": {
"onyx:usesEmotionModel": {
"type": "array",
"items": {
"$ref": "emotionModel.json"
}
}
}
}
]
}

View File

@@ -1,4 +1,24 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "definitions.json#/EmotionSet" "properties": {
"@id": {"type": "string"},
"nif:beginIndex": {"type": "integer"},
"nif:endIndex": {"type": "integer"},
"nif:anchorOf": {
"description": "Piece of context that contains the Sentiment",
"type": "string"
},
"onyx:hasEmotion": {
"type": "array",
"items": {
"$ref": "emotion.json"
},
"default": []
},
"prov:wasGeneratedBy": {
"type": "string",
"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"]
} }

View File

@@ -0,0 +1,4 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object"
}

View File

@@ -1,4 +1,39 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "definitions.json#/Entry" "name": "Entry",
"properties": {
"@id": {
"type": "string"
},
"nif:isString": {
"description": "String contained in this Context",
"type": "string"
},
"sentiments": {
"type": "array",
"items": {"$ref": "sentiment.json" },
"default": []
},
"emotions": {
"type": "array",
"items": {"$ref": "emotionSet.json" },
"default": []
},
"entities": {
"type": "array",
"items": {"$ref": "entity.json" },
"default": []
},
"topics": {
"type": "array",
"items": {"$ref": "topic.json" },
"default": []
},
"suggestions": {
"type": "array",
"items": {"$ref": "suggestion.json" },
"default": []
}
},
"required": ["@id", "nif:isString"]
} }

23
senpy/schemas/error.json Normal file
View File

@@ -0,0 +1,23 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Base schema for all Senpy objects",
"type": "object",
"allOf": [
{"$ref": "atom.json"},
{
"properties": {
"message": {
"type": "string"
},
"errors": {
"type": "array",
"items": {"type": "object"}
},
"status": {
"type": "number"
}
},
"required": ["message"]
}
]
}

View File

@@ -1,3 +1,19 @@
{ {
"$ref": "definitions.json#/Plugin" "$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"required": ["@id", "extra_params"],
"properties": {
"@id": {
"type": "string",
"description": "Unique identifier for the plugin, usually comprised of the name of the plugin and the version."
},
"name": {
"type": "string",
"description": "The name of the plugin, which will be used in the algorithm detection phase"
},
"extra_params": {
"type": "object",
"default": {}
}
}
} }

View File

@@ -1,3 +1,18 @@
{ {
"$ref": "definitions.json#/Plugins" "$schema": "http://json-schema.org/draft-04/schema#",
"allOf": [
{"$ref": "response.json"},
{
"properties": {
"plugins": {
"type": "array",
"items": {
"$ref": "plugin.json"
}
},
"@type": {
}
}
}
]
} }

View File

@@ -1,4 +1,9 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "definitions.json#/Response" "type": "object",
"properties": {
"@type": {"type": "string"}
},
"required": ["@type"]
} }

View File

@@ -1,4 +1,39 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "definitions.json#/Results" "allOf": [
{"$ref": "response.json"},
{
"title": "Results",
"description": "The results of an analysis",
"type": "object",
"properties": {
"@context": {
"$ref": "context.json"
},
"@type": {
"default": "results"
},
"@id": {
"description": "ID of the analysis",
"type": "string"
},
"analysis": {
"type": "array",
"default": [],
"items": {
"$ref": "analysis.json"
}
},
"entries": {
"type": "array",
"default": [],
"items": {
"$ref": "entry.json"
}
}
},
"required": ["@id", "analysis", "entries"]
}
]
} }

View File

@@ -1,4 +1,23 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "definitions.json#/Sentiment" "properties": {
"@id": {"type": "string"},
"nif:beginIndex": {"type": "integer"},
"nif:endIndex": {"type": "integer"},
"nif:anchorOf": {
"description": "Piece of context that contains the Sentiment",
"type": "string"
},
"marl:hasPolarity": {
"enum": ["marl:Positive", "marl:Negative", "marl:Neutral"]
},
"marl:polarityValue": {
"type": "number"
},
"prov:wasGeneratedBy": {
"type": "string",
"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"]
} }

View File

@@ -0,0 +1,19 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"allOf": [
{
"$ref": "plugin.json"
},
{
"properties": {
"marl:minPolarityValue": {
"type": "number"
},
"marl:maxPolarityValue": {
"type": "number"
}
}
}
]
}

View File

@@ -1,4 +1,5 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "definitions.json#/Suggestion" "type": "object",
"required": ["@id", "prov:wasGeneratedBy"]
} }

4
senpy/schemas/topic.json Normal file
View File

@@ -0,0 +1,4 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object"
}

View File

@@ -0,0 +1,893 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="216"
height="144"
id="svg4136"
version="1.1"
inkscape:version="0.91 r"
sodipodi:docname="jsoneditor-icons.svg">
<title
id="title6512">JSON Editor Icons</title>
<metadata
id="metadata4148">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>JSON Editor Icons</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4146" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1028"
id="namedview4144"
showgrid="true"
inkscape:zoom="4"
inkscape:cx="97.217248"
inkscape:cy="59.950227"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg4136"
showguides="false"
borderlayer="false"
inkscape:showpageshadow="true"
showborder="true">
<inkscape:grid
type="xygrid"
id="grid4640"
empspacing="24" />
</sodipodi:namedview>
<!-- Created with SVG-edit - http://svg-edit.googlecode.com/ -->
<g
id="g4394">
<rect
x="4"
y="4"
width="16"
height="16"
id="svg_1"
style="fill:#1aae1c;fill-opacity:1;stroke:none;stroke-width:0" />
<rect
style="fill:#ec3f29;fill-opacity:0.94117647;stroke:none;stroke-width:0"
x="28.000006"
y="3.999995"
width="16"
height="16"
id="svg_1-7" />
<rect
id="rect4165"
height="16"
width="16"
y="3.999995"
x="52.000004"
style="fill:#4c4c4c;fill-opacity:1;stroke:none;stroke-width:0" />
<rect
style="fill:#4c4c4c;fill-opacity:1;stroke:none;stroke-width:0"
x="172.00002"
y="3.9999852"
width="16"
height="16"
id="rect4175" />
<rect
style="fill:#4c4c4c;fill-opacity:1;stroke:none;stroke-width:0"
x="196"
y="3.999995"
width="16"
height="16"
id="rect4175-3" />
<g
style="stroke:none"
id="g4299">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="svg_1-1"
height="1.9999986"
width="9.9999924"
y="10.999998"
x="7.0000048" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="svg_1-1-1"
height="9.9999838"
width="1.9999955"
y="7.0000114"
x="11.000005" />
</g>
<g
style="stroke:none"
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,19.029435,12.000001)"
id="g4299-3">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="svg_1-1-0"
height="1.9999986"
width="9.9999924"
y="10.999998"
x="7.0000048" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="svg_1-1-1-9"
height="9.9999838"
width="1.9999955"
y="7.0000114"
x="11.000005" />
</g>
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="55.000004"
y="7.0000048"
width="6.9999909"
height="6.9999905"
id="svg_1-7-5" />
<rect
id="rect4354"
height="6.9999905"
width="6.9999909"
y="10.00001"
x="58"
style="fill:#ffffff;fill-opacity:1;stroke:#4c4c4c;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#3c80df;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.94117647"
x="58.000004"
y="10.000005"
width="6.9999909"
height="6.9999905"
id="svg_1-7-5-7" />
<g
id="g4378">
<rect
id="svg_1-7-5-3"
height="1.9999965"
width="7.9999909"
y="10.999999"
x="198"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="198"
y="7.0000005"
width="11.999995"
height="1.9999946"
id="rect4374" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="198"
y="14.999996"
width="3.9999928"
height="1.9999995"
id="rect4376" />
</g>
<g
id="g4383"
transform="matrix(1,0,0,-1,-23.999995,23.999995)">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="198"
y="10.999999"
width="7.9999909"
height="1.9999965"
id="rect4385" />
<rect
id="rect4387"
height="1.9999946"
width="11.999995"
y="7.0000005"
x="198"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" />
<rect
id="rect4389"
height="1.9999995"
width="3.9999928"
y="14.999996"
x="198"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" />
</g>
<rect
y="3.9999199"
x="76"
height="16"
width="16"
id="rect3754-4"
style="fill:#4c4c4c;fill-opacity:1;stroke:none" />
<path
sodipodi:nodetypes="cccccccc"
inkscape:connector-curvature="0"
id="path4351"
d="m 85.10447,6.0157384 -0.0156,1.4063 c 3.02669,-0.2402 0.33008,3.6507996 2.48438,4.5780996 -2.18694,1.0938 0.49191,4.9069 -2.45313,4.5781 l -0.0156,1.4219 c 5.70828,0.559 1.03264,-5.1005 4.70313,-5.2656 l 0,-1.4063 c -3.61303,-0.027 1.11893,-5.7069996 -4.70313,-5.3124996 z"
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cccccccc"
inkscape:connector-curvature="0"
id="path4351-9"
d="m 82.78125,5.9984384 0.0156,1.4063 c -3.02668,-0.2402 -0.33007,3.6506996 -2.48437,4.5780996 2.18694,1.0938 -0.49192,4.9069 2.45312,4.5781 l 0.0156,1.4219 c -5.70827,0.559 -1.03263,-5.1004 -4.70312,-5.2656 l 0,-1.4063 c 3.61303,-0.027 -1.11894,-5.7070996 4.70312,-5.3124996 z"
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="3.9999199"
x="100"
height="16"
width="16"
id="rect3754-25"
style="fill:#4c4c4c;fill-opacity:1;stroke:none" />
<path
inkscape:connector-curvature="0"
id="path2987"
d="m 103.719,5.6719384 0,12.7187996 3.03125,0 0,-1.5313 -1.34375,0 0,-9.6249996 1.375,0 0,-1.5625 z"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
<path
inkscape:connector-curvature="0"
id="path2987-1"
d="m 112.2185,5.6721984 0,12.7187996 -3.03125,0 0,-1.5313 1.34375,0 0,-9.6249996 -1.375,0 0,-1.5625 z"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
<rect
y="3.9999199"
x="124"
height="16"
width="16"
id="rect3754-73"
style="fill:#4c4c4c;fill-opacity:1;stroke:none" />
<path
sodipodi:nodetypes="ccccccccc"
inkscape:connector-curvature="0"
id="path3780"
d="m 126.2824,17.602938 1.78957,0 1.14143,-2.8641 5.65364,0 1.14856,2.8641 1.76565,0 -4.78687,-11.1610996 -1.91903,0 z"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
<path
inkscape:connector-curvature="0"
id="path3782"
d="m 129.72704,13.478838 4.60852,0.01 -2.30426,-5.5497996 z"
style="fill:#4c4c4c;fill-opacity:1;stroke:none" />
<rect
y="3.9999199"
x="148"
height="16"
width="16"
id="rect3754-35"
style="fill:#4c4c4c;fill-opacity:1;stroke:none" />
<path
sodipodi:nodetypes="ccccccc"
inkscape:connector-curvature="0"
id="path5008-2"
d="m 156.47655,5.8917384 0,2.1797 0.46093,2.3983996 1.82813,0 0.39844,-2.3983996 0,-2.1797 z"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
<path
sodipodi:nodetypes="ccccccc"
inkscape:connector-curvature="0"
id="path5008-2-8"
d="m 152.51561,5.8906384 0,2.1797 0.46094,2.3983996 1.82812,0 0.39844,-2.3983996 0,-2.1797 z"
style="fill:#ffffff;fill-opacity:1;stroke:none" />
</g>
<rect
x="4"
y="27.999994"
width="16"
height="16"
id="rect4432"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0"
x="28.000006"
y="27.99999"
width="16"
height="16"
id="rect4434" />
<rect
id="rect4436"
height="16"
width="16"
y="27.99999"
x="52.000004"
style="fill:#d3d3d3;fill-opacity:1;stroke:#000000;stroke-width:0" />
<rect
style="fill:#d3d3d3;stroke:#000000;stroke-width:0"
x="172.00002"
y="27.999981"
width="16"
height="16"
id="rect4446" />
<rect
style="fill:#d3d3d3;stroke:#000000;stroke-width:0"
x="196"
y="27.99999"
width="16"
height="16"
id="rect4448" />
<g
id="g4466"
style="stroke:none"
transform="translate(0,23.999995)">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="rect4468"
height="1.9999986"
width="9.9999924"
y="10.999998"
x="7.0000048" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="rect4470"
height="9.9999838"
width="1.9999955"
y="7.0000114"
x="11.000005" />
</g>
<g
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,19.029435,35.999996)"
id="g4472"
style="stroke:none">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="rect4474"
height="1.9999986"
width="9.9999924"
y="10.999998"
x="7.0000048" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0"
id="rect4476"
height="9.9999838"
width="1.9999955"
y="7.0000114"
x="11.000005" />
</g>
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="55.000004"
y="31"
width="6.9999909"
height="6.9999905"
id="rect4478" />
<rect
id="rect4480"
height="6.9999905"
width="6.9999909"
y="34.000008"
x="58"
style="fill:#ffffff;fill-opacity:1;stroke:#d3d3d3;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#d3d3d3;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none"
x="58.000004"
y="34.000004"
width="6.9999909"
height="6.9999905"
id="rect4482" />
<g
id="g4484"
transform="translate(0,23.999995)">
<rect
id="rect4486"
height="1.9999965"
width="7.9999909"
y="10.999999"
x="198"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="198"
y="7.0000005"
width="11.999995"
height="1.9999946"
id="rect4488" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="198"
y="14.999996"
width="3.9999928"
height="1.9999995"
id="rect4490" />
</g>
<g
id="g4492"
transform="matrix(1,0,0,-1,-23.999995,47.99999)">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0"
x="198"
y="10.999999"
width="7.9999909"
height="1.9999965"
id="rect4494" />
<rect
id="rect4496"
height="1.9999946"
width="11.999995"
y="7.0000005"
x="198"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" />
<rect
id="rect4498"
height="1.9999995"
width="3.9999928"
y="14.999996"
x="198"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" />
</g>
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none"
id="rect3754-8"
width="16"
height="16"
x="76"
y="27.99992" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 85.10448,30.015537 -0.0156,1.4063 c 3.02668,-0.2402 0.33007,3.6508 2.48438,4.5781 -2.18695,1.0938 0.49191,4.90688 -2.45313,4.57808 l -0.0156,1.4219 c 5.70827,0.559 1.03263,-5.10048 4.70313,-5.26558 l 0,-1.4063 c -3.61304,-0.027 1.11893,-5.707 -4.70313,-5.3125 z"
id="path4351-1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccc" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 82.78126,29.998237 0.0156,1.4063 c -3.02668,-0.2402 -0.33008,3.6507 -2.48438,4.5781 2.18694,1.0938 -0.49191,4.90688 2.45313,4.57808 l 0.0156,1.4219 c -5.70828,0.559 -1.03264,-5.10038 -4.70313,-5.26558 l 0,-1.4063 c 3.61303,-0.027 -1.11893,-5.7071 4.70313,-5.3125 z"
id="path4351-9-5"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccc" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none"
id="rect3754-65"
width="16"
height="16"
x="100"
y="27.99992" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:none"
d="m 103.719,29.671937 0,12.71878 3.03125,0 0,-1.5313 -1.34375,0 0,-9.62498 1.375,0 0,-1.5625 z"
id="path2987-8"
inkscape:connector-curvature="0" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:none"
d="m 112.2185,29.671937 0,12.71878 -3.03125,0 0,-1.5313 1.34375,0 0,-9.62498 -1.375,0 0,-1.5625 z"
id="path2987-1-9"
inkscape:connector-curvature="0" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none"
id="rect3754-92"
width="16"
height="16"
x="124"
y="27.99992" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:none"
d="m 126.2824,41.602917 1.78957,0 1.14143,-2.86408 5.65364,0 1.14856,2.86408 1.76565,0 -4.78687,-11.16108 -1.91902,0 z"
id="path3780-9"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
<path
style="fill:#d3d3d3;fill-opacity:1;stroke:none"
d="m 129.72704,37.478837 4.60852,0.01 -2.30426,-5.5498 z"
id="path3782-2"
inkscape:connector-curvature="0" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none"
id="rect3754-47"
width="16"
height="16"
x="148"
y="27.99992" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:none"
d="m 156.47656,29.891737 0,2.1797 0.46093,2.3984 1.82813,0 0.39844,-2.3984 0,-2.1797 z"
id="path5008-2-1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:none"
d="m 152.51562,29.890637 0,2.1797 0.46094,2.3984 1.82812,0 0.39844,-2.3984 0,-2.1797 z"
id="path5008-2-8-8"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<rect
id="svg_1-7-2"
height="1.9999961"
width="11.999996"
y="64"
x="54"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0" />
<rect
id="svg_1-7-2-2"
height="2.9999905"
width="2.9999907"
y="52"
x="80.000008"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0" />
<rect
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
x="85.000008"
y="52"
width="2.9999907"
height="2.9999905"
id="rect4561" />
<rect
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
x="80.000008"
y="58"
width="2.9999907"
height="2.9999905"
id="rect4563" />
<rect
id="rect4565"
height="2.9999905"
width="2.9999907"
y="58"
x="85.000008"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0" />
<rect
id="rect4567"
height="2.9999905"
width="2.9999907"
y="64"
x="80.000008"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0" />
<rect
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
x="85.000008"
y="64"
width="2.9999907"
height="2.9999905"
id="rect4569" />
<circle
style="opacity:1;fill:none;fill-opacity:1;stroke:#4c4c4c;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
id="path4571"
cx="110.06081"
cy="57.939209"
r="4.7438836" />
<rect
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
x="116.64566"
y="-31.79752"
width="4.229713"
height="6.4053884"
id="rect4563-2"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
<path
style="fill:#4c4c4c;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 125,56 138.77027,56.095 132,64 Z"
id="path4613"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
sodipodi:nodetypes="cccc"
inkscape:connector-curvature="0"
id="path4615"
d="M 149,64 162.77027,63.905 156,56 Z"
style="fill:#4c4c4c;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
x="54"
y="53"
width="11.999996"
height="1.9999961"
id="rect4638" />
<rect
id="svg_1-7-2-24"
height="1.9999957"
width="12.99999"
y="-56"
x="53"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
transform="matrix(0,1,-1,0,0,0)" />
<rect
transform="matrix(0,1,-1,0,0,0)"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0"
x="53"
y="-66"
width="12.99999"
height="1.9999957"
id="rect4657" />
<rect
id="rect4659"
height="0.99999291"
width="11.999999"
y="57"
x="54"
style="fill:#4c4c4c;fill-opacity:0.98431373;stroke:none;stroke-width:0" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
x="54"
y="88.000122"
width="11.999996"
height="1.9999961"
id="rect4661" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
x="80.000008"
y="76.000122"
width="2.9999907"
height="2.9999905"
id="rect4663" />
<rect
id="rect4665"
height="2.9999905"
width="2.9999907"
y="76.000122"
x="85.000008"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1" />
<rect
id="rect4667"
height="2.9999905"
width="2.9999907"
y="82.000122"
x="80.000008"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
x="85.000008"
y="82.000122"
width="2.9999907"
height="2.9999905"
id="rect4669" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
x="80.000008"
y="88.000122"
width="2.9999907"
height="2.9999905"
id="rect4671" />
<rect
id="rect4673"
height="2.9999905"
width="2.9999907"
y="88.000122"
x="85.000008"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1" />
<circle
r="4.7438836"
cy="81.939331"
cx="110.06081"
id="circle4675"
style="opacity:1;fill:none;fill-opacity:1;stroke:#d3d3d3;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)"
id="rect4677"
height="6.4053884"
width="4.229713"
y="-14.826816"
x="133.6163"
style="fill:#d3d3d3;fill-opacity:1;stroke:#d3d3d3;stroke-width:0;stroke-opacity:1" />
<path
sodipodi:nodetypes="cccc"
inkscape:connector-curvature="0"
id="path4679"
d="m 125,80.000005 13.77027,0.09499 L 132,87.999992 Z"
style="fill:#d3d3d3;fill-opacity:1;fill-rule:evenodd;stroke:#d3d3d3;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:#d3d3d3;fill-opacity:1;fill-rule:evenodd;stroke:#d3d3d3;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 149,88.0002 162.77027,87.9052 156,80.0002 Z"
id="path4681"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<rect
id="rect4683"
height="1.9999961"
width="11.999996"
y="77.000122"
x="54"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1" />
<rect
transform="matrix(0,1,-1,0,0,0)"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
x="77.000122"
y="-56"
width="12.99999"
height="1.9999957"
id="rect4685" />
<rect
id="rect4687"
height="1.9999957"
width="12.99999"
y="-66"
x="77.000122"
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
transform="matrix(0,1,-1,0,0,0)" />
<rect
style="fill:#d3d3d3;fill-opacity:1;stroke:none;stroke-width:0;stroke-opacity:1"
x="54"
y="81.000122"
width="11.999999"
height="0.99999291"
id="rect4689" />
<rect
id="rect4761-1"
height="1.9999945"
width="15.99999"
y="101"
x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<rect
id="rect4761-0"
height="1.9999945"
width="15.99999"
y="105"
x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<rect
id="rect4761-7"
height="1.9999945"
width="9"
y="109"
x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<rect
id="rect4761-1-1"
height="1.9999945"
width="12"
y="125"
x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<rect
id="rect4761-1-1-4"
height="1.9999945"
width="10"
y="137"
x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<rect
id="rect4761-1-1-4-4"
height="1.9999945"
width="10"
y="129"
x="82"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<rect
id="rect4761-1-1-4-4-3"
height="1.9999945"
width="9"
y="133"
x="82"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" />
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.8;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.66157866;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 36.398438,100.0254 c -0.423362,-0.013 -0.846847,0.01 -1.265626,0.062 -1.656562,0.2196 -3.244567,0.9739 -4.507812,2.2266 L 29,100.5991 l -2.324219,7.7129 7.826172,-1.9062 -1.804687,-1.9063 c 1.597702,-1.5308 4.048706,-1.8453 5.984375,-0.7207 1.971162,1.1452 2.881954,3.3975 2.308593,5.5508 -0.573361,2.1533 -2.533865,3.6953 -4.830078,3.6953 l 0,3.0742 c 3.550756,0 6.710442,-2.4113 7.650391,-5.9414 0.939949,-3.5301 -0.618463,-7.2736 -3.710938,-9.0703 -1.159678,-0.6738 -2.431087,-1.0231 -3.701171,-1.0625 z"
id="path4138" />
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.8;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.66157866;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 59.722656,99.9629 c -1.270084,0.039 -2.541493,0.3887 -3.701172,1.0625 -3.092475,1.7967 -4.650886,5.5402 -3.710937,9.0703 0.939949,3.5301 4.09768,5.9414 7.648437,5.9414 l 0,-3.0742 c -2.296214,0 -4.256717,-1.542 -4.830078,-3.6953 -0.573361,-2.1533 0.337432,-4.4056 2.308594,-5.5508 1.935731,-1.1246 4.38863,-0.8102 5.986326,0.7207 l -1.806638,1.9063 7.828128,1.9062 -2.32422,-7.7129 -1.62696,1.7168 c -1.26338,-1.2531 -2.848917,-2.0088 -4.505855,-2.2285 -0.418778,-0.055 -0.842263,-0.076 -1.265625,-0.062 z"
id="path4138-1" />
<path
inkscape:connector-curvature="0"
style="opacity:0.8;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.966;stroke-miterlimit:4;stroke-dasharray:none"
d="m 10.5,100 0,2 -2.4999996,0 L 12,107 l 4,-5 -2.5,0 0,-2 -3,0 z"
id="path3055-0-77" />
<path
style="opacity:0.8;fill:none;stroke:#ffffff;stroke-width:1.966;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 4.9850574,108.015 14.0298856,-0.03"
id="path5244-5-0-5"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="opacity:0.8;fill:none;stroke:#ffffff;stroke-width:1.966;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 4.9849874,132.015 14.0298866,-0.03"
id="path5244-5-0-5-8"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.4;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#4d4d4d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.66157866;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 36.398438,123.9629 c -0.423362,-0.013 -0.846847,0.01 -1.265626,0.062 -1.656562,0.2196 -3.244567,0.9739 -4.507812,2.2266 L 29,124.5366 l -2.324219,7.7129 7.826172,-1.9062 -1.804687,-1.9063 c 1.597702,-1.5308 4.048706,-1.8453 5.984375,-0.7207 1.971162,1.1453 2.881954,3.3975 2.308593,5.5508 -0.573361,2.1533 -2.533864,3.6953 -4.830078,3.6953 l 0,3.0742 c 3.550757,0 6.710442,-2.4093 7.650391,-5.9394 0.939949,-3.5301 -0.618463,-7.2756 -3.710938,-9.0723 -1.159678,-0.6737 -2.431087,-1.0231 -3.701171,-1.0625 z"
id="path4138-12" />
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.4;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#4d4d4d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.66157866;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 59.722656,123.9629 c -1.270084,0.039 -2.541493,0.3888 -3.701172,1.0625 -3.092475,1.7967 -4.650886,5.5422 -3.710937,9.0723 0.939949,3.5301 4.09768,5.9394 7.648437,5.9394 l 0,-3.0742 c -2.296214,0 -4.256717,-1.542 -4.830078,-3.6953 -0.573361,-2.1533 0.337432,-4.4055 2.308594,-5.5508 1.935731,-1.1246 4.38863,-0.8102 5.986326,0.7207 l -1.806638,1.9063 7.828128,1.9062 -2.32422,-7.7129 -1.62696,1.7168 c -1.26338,-1.2531 -2.848917,-2.0088 -4.505855,-2.2285 -0.418778,-0.055 -0.842263,-0.076 -1.265625,-0.062 z"
id="path4138-1-3" />
<path
id="path6191"
d="m 10.5,116 0,-2 -2.4999996,0 L 12,109 l 4,5 -2.5,0 0,2 -3,0 z"
style="opacity:0.8;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.966;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
style="opacity:0.8;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.966;stroke-miterlimit:4;stroke-dasharray:none"
d="m 10.5,129 0,-2 -2.4999996,0 L 12,122 l 4,5 -2.5,0 0,2 -3,0 z"
id="path6193" />
<path
id="path6195"
d="m 10.5,135 0,2 -2.4999996,0 L 12,142 l 4,-5 -2.5,0 0,-2 -3,0 z"
style="opacity:0.8;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.966;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0" />
<path
sodipodi:type="star"
style="fill:#4d4d4d;fill-opacity:0.90196078;stroke:#d3d3d3;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
id="path4500"
sodipodi:sides="3"
sodipodi:cx="11.55581"
sodipodi:cy="60.073242"
sodipodi:r1="5.1116104"
sodipodi:r2="2.5558052"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 16.66742,60.073242 -3.833708,2.213392 -3.8337072,2.213393 0,-4.426785 0,-4.426784 3.8337082,2.213392 z"
inkscape:transform-center-x="-1.2779026" />
<path
inkscape:transform-center-x="1.277902"
d="m -31.500004,60.073242 -3.833708,2.213392 -3.833707,2.213393 0,-4.426785 0,-4.426784 3.833707,2.213392 z"
inkscape:randomized="0"
inkscape:rounded="0"
inkscape:flatsided="false"
sodipodi:arg2="1.0471976"
sodipodi:arg1="0"
sodipodi:r2="2.5558052"
sodipodi:r1="5.1116104"
sodipodi:cy="60.073242"
sodipodi:cx="-36.611614"
sodipodi:sides="3"
id="path4502"
style="fill:#4d4d4d;fill-opacity:0.90196078;stroke:#d3d3d3;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="star"
transform="scale(-1,1)" />
<path
d="m 16.66742,60.073212 -3.833708,2.213392 -3.8337072,2.213392 0,-4.426784 0,-4.426785 3.8337082,2.213392 z"
inkscape:randomized="0"
inkscape:rounded="0"
inkscape:flatsided="false"
sodipodi:arg2="1.0471976"
sodipodi:arg1="0"
sodipodi:r2="2.5558052"
sodipodi:r1="5.1116104"
sodipodi:cy="60.073212"
sodipodi:cx="11.55581"
sodipodi:sides="3"
id="path4504"
style="fill:#4d4d4d;fill-opacity:0.90196078;stroke:#d3d3d3;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="star"
transform="matrix(0,1,-1,0,72.0074,71.7877)"
inkscape:transform-center-y="1.2779029" />
<path
inkscape:transform-center-y="-1.2779026"
transform="matrix(0,-1,-1,0,96,96)"
sodipodi:type="star"
style="fill:#4d4d4d;fill-opacity:0.90196078;stroke:#d3d3d3;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
id="path4506"
sodipodi:sides="3"
sodipodi:cx="11.55581"
sodipodi:cy="60.073212"
sodipodi:r1="5.1116104"
sodipodi:r2="2.5558052"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 16.66742,60.073212 -3.833708,2.213392 -3.8337072,2.213392 0,-4.426784 0,-4.426785 3.8337082,2.213392 z" />
<path
sodipodi:nodetypes="cccc"
inkscape:connector-curvature="0"
id="path4615-5"
d="m 171.82574,65.174193 16.34854,0 -8.17427,-13.348454 z"
style="fill:#fbb917;fill-opacity:1;fill-rule:evenodd;stroke:#fbb917;stroke-width:1.65161395;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 179,55 0,6 2,0 0,-6"
id="path4300"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 179,62 0,2 2,0 0,-2"
id="path4300-6"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
</svg>

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -0,0 +1,449 @@
div.jsoneditor {
}
div.jsoneditor-field,
div.jsoneditor-value,
div.jsoneditor-readonly {
border: 1px solid transparent;
min-height: 16px;
min-width: 32px;
padding: 2px;
margin: 1px;
word-wrap: break-word;
float: left;
}
/* adjust margin of p elements inside editable divs, needed for Opera, IE */
div.jsoneditor-field p,
div.jsoneditor-value p {
margin: 0;
}
div.jsoneditor-value {
word-break: break-word;
}
div.jsoneditor-readonly {
min-width: 16px;
color: gray;
}
div.jsoneditor-empty {
border-color: lightgray;
border-style: dashed;
border-radius: 2px;
}
div.jsoneditor-field.jsoneditor-empty::after,
div.jsoneditor-value.jsoneditor-empty::after {
pointer-events: none;
color: lightgray;
font-size: 8pt;
}
div.jsoneditor-field.jsoneditor-empty::after {
content: "field";
}
div.jsoneditor-value.jsoneditor-empty::after {
content: "value";
}
div.jsoneditor-value.jsoneditor-url,
a.jsoneditor-value.jsoneditor-url {
color: green;
text-decoration: underline;
}
a.jsoneditor-value.jsoneditor-url {
display: inline-block;
padding: 2px;
margin: 2px;
}
a.jsoneditor-value.jsoneditor-url:hover,
a.jsoneditor-value.jsoneditor-url:focus {
color: #ee422e;
}
div.jsoneditor td.jsoneditor-separator {
padding: 3px 0;
vertical-align: top;
color: gray;
}
div.jsoneditor-field[contenteditable=true]:focus,
div.jsoneditor-field[contenteditable=true]:hover,
div.jsoneditor-value[contenteditable=true]:focus,
div.jsoneditor-value[contenteditable=true]:hover,
div.jsoneditor-field.jsoneditor-highlight,
div.jsoneditor-value.jsoneditor-highlight {
background-color: #FFFFAB;
border: 1px solid yellow;
border-radius: 2px;
}
div.jsoneditor-field.jsoneditor-highlight-active,
div.jsoneditor-field.jsoneditor-highlight-active:focus,
div.jsoneditor-field.jsoneditor-highlight-active:hover,
div.jsoneditor-value.jsoneditor-highlight-active,
div.jsoneditor-value.jsoneditor-highlight-active:focus,
div.jsoneditor-value.jsoneditor-highlight-active:hover {
background-color: #ffee00;
border: 1px solid #ffc700;
border-radius: 2px;
}
div.jsoneditor-value.jsoneditor-string {
color: #008000;
}
div.jsoneditor-value.jsoneditor-object,
div.jsoneditor-value.jsoneditor-array {
min-width: 16px;
color: #808080;
}
div.jsoneditor-value.jsoneditor-number {
color: #ee422e;
}
div.jsoneditor-value.jsoneditor-boolean {
color: #ff8c00;
}
div.jsoneditor-value.jsoneditor-null {
color: #004ED0;
}
div.jsoneditor-value.jsoneditor-invalid {
color: #000000;
}
div.jsoneditor-tree button {
width: 24px;
height: 24px;
padding: 0;
margin: 0;
border: none;
cursor: pointer;
background: transparent url('img/jsoneditor-icons.svg');
}
div.jsoneditor-mode-view tr.jsoneditor-expandable td.jsoneditor-tree,
div.jsoneditor-mode-form tr.jsoneditor-expandable td.jsoneditor-tree {
cursor: pointer;
}
div.jsoneditor-tree button.jsoneditor-collapsed {
background-position: 0 -48px;
}
div.jsoneditor-tree button.jsoneditor-expanded {
background-position: 0 -72px;
}
div.jsoneditor-tree button.jsoneditor-contextmenu {
background-position: -48px -72px;
}
div.jsoneditor-tree button.jsoneditor-contextmenu:hover,
div.jsoneditor-tree button.jsoneditor-contextmenu:focus,
div.jsoneditor-tree button.jsoneditor-contextmenu.jsoneditor-selected,
tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-contextmenu {
background-position: -48px -48px;
}
div.jsoneditor-menu {
display:none;
}
div.jsoneditor-tree *:focus {
outline: none;
}
div.jsoneditor-tree button:focus {
/* TODO: nice outline for buttons with focus
outline: #97B0F8 solid 2px;
box-shadow: 0 0 8px #97B0F8;
*/
background-color: #f5f5f5;
outline: #e5e5e5 solid 1px;
}
div.jsoneditor-tree button.jsoneditor-invisible {
visibility: hidden;
background: none;
}
div.jsoneditor {
color: #1A1A1A;
border: 0px solid #3883fa;
-moz-box-sizing: border-box;
box-sizing: border-box;
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
padding: 0;
line-height: 100%;
}
div.jsoneditor-tree table.jsoneditor-tree {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
margin: 0;
}
div.jsoneditor-outer {
position: static;
width: 100%;
height: 100%;
margin: -35px 0 0 0;
padding: 35px 0 0 0;
}
textarea.jsoneditor-text,
.ace-jsoneditor {
min-height: 150px;
}
div.jsoneditor-tree {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
textarea.jsoneditor-text {
width: 100%;
height: 100%;
margin: 0;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
outline-width: 0;
border: none;
background-color: white;
resize: none;
}
tr.jsoneditor-highlight,
tr.jsoneditor-selected {
background-color: #e6e6e6;
}
tr.jsoneditor-selected button.jsoneditor-dragarea,
tr.jsoneditor-selected button.jsoneditor-contextmenu {
visibility: hidden;
}
tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-dragarea,
tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-contextmenu {
visibility: visible;
}
div.jsoneditor-tree button.jsoneditor-dragarea {
background: url('img/jsoneditor-icons.svg') -72px -72px;
cursor: move;
}
div.jsoneditor-tree button.jsoneditor-dragarea:hover,
div.jsoneditor-tree button.jsoneditor-dragarea:focus,
tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-dragarea {
background-position: -72px -48px;
}
div.jsoneditor tr,
div.jsoneditor th,
div.jsoneditor td {
padding: 0;
margin: 0;
}
div.jsoneditor td {
vertical-align: top;
}
div.jsoneditor td.jsoneditor-tree {
vertical-align: top;
}
div.jsoneditor-field,
div.jsoneditor-value,
div.jsoneditor td,
div.jsoneditor th,
div.jsoneditor textarea,
.jsoneditor-schema-error {
font-family: droid sans mono, consolas, monospace, courier new, courier, sans-serif;
font-size: 10pt;
color: #1A1A1A;
}
/* popover */
.jsoneditor-schema-error {
cursor: default;
display: inline-block;
/*font-family: arial, sans-serif;*/
height: 24px;
line-height: 24px;
position: relative;
text-align: center;
width: 24px;
}
div.jsoneditor-tree .jsoneditor-schema-error {
width: 24px;
height: 24px;
padding: 0;
margin: 0 4px 0 0;
background: url('img/jsoneditor-icons.svg') -168px -48px;
}
.jsoneditor-schema-error .jsoneditor-popover {
background-color: #4c4c4c;
border-radius: 3px;
box-shadow: 0 0 5px rgba(0,0,0,0.4);
color: #fff;
display: none;
padding: 7px 10px;
position: absolute;
width: 200px;
z-index: 4;
}
.jsoneditor-schema-error .jsoneditor-popover.jsoneditor-above {
bottom: 32px;
left: -98px;
}
.jsoneditor-schema-error .jsoneditor-popover.jsoneditor-below {
top: 32px;
left: -98px;
}
.jsoneditor-schema-error .jsoneditor-popover.jsoneditor-left {
top: -7px;
right: 32px;
}
.jsoneditor-schema-error .jsoneditor-popover.jsoneditor-right {
top: -7px;
left: 32px;
}
.jsoneditor-schema-error .jsoneditor-popover:before {
border-right: 7px solid transparent;
border-left: 7px solid transparent;
content: '';
display: block;
left: 50%;
margin-left: -7px;
position: absolute;
}
.jsoneditor-schema-error .jsoneditor-popover.jsoneditor-above:before {
border-top: 7px solid #4c4c4c;
bottom: -7px;
}
.jsoneditor-schema-error .jsoneditor-popover.jsoneditor-below:before {
border-bottom: 7px solid #4c4c4c;
top: -7px;
}
.jsoneditor-schema-error .jsoneditor-popover.jsoneditor-left:before {
border-left: 7px solid #4c4c4c;
border-top: 7px solid transparent;
border-bottom: 7px solid transparent;
content: '';
top: 19px;
right: -14px;
left: inherit;
margin-left: inherit;
margin-top: -7px;
position: absolute;
}
.jsoneditor-schema-error .jsoneditor-popover.jsoneditor-right:before {
border-right: 7px solid #4c4c4c;
border-top: 7px solid transparent;
border-bottom: 7px solid transparent;
content: '';
top: 19px;
left: -14px;
margin-left: inherit;
margin-top: -7px;
position: absolute;
}
.jsoneditor-schema-error:hover .jsoneditor-popover,
.jsoneditor-schema-error:focus .jsoneditor-popover {
display: block;
-webkit-animation: fade-in .3s linear 1, move-up .3s linear 1;
-moz-animation: fade-in .3s linear 1, move-up .3s linear 1;
-ms-animation: fade-in .3s linear 1, move-up .3s linear 1;
}
@-webkit-keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@-moz-keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@-ms-keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
/*@-webkit-keyframes move-up {*/
/*from { bottom: 24px; }*/
/*to { bottom: 32px; }*/
/*}*/
/*@-moz-keyframes move-up {*/
/*from { bottom: 24px; }*/
/*to { bottom: 32px; }*/
/*}*/
/*@-ms-keyframes move-up {*/
/*from { bottom: 24px; }*/
/*to { bottom: 32px; }*/
/*}*/
/* JSON schema errors displayed at the bottom of the editor in mode text and code */
.jsoneditor .jsoneditor-text-errors {
width: 100%;
border-collapse: collapse;
background-color: #ffef8b;
border-top: 1px solid #ffd700;
}
.jsoneditor .jsoneditor-text-errors td {
padding: 3px 6px;
vertical-align: middle;
}
.jsoneditor-text-errors .jsoneditor-schema-error {
border: none;
width: 24px;
height: 24px;
padding: 0;
margin: 0 4px 0 0;
background: url('img/jsoneditor-icons.svg') -168px -48px;
}

View File

@@ -8,7 +8,7 @@ body {
} }
#inputswrapper { #inputswrapper {
min-height:100%; min-height:100%;
background: white; /* background: white; */
position:relative; position:relative;
min-width: 800px; min-width: 800px;
height: 100%; height: 100%;
@@ -50,23 +50,31 @@ body {
#form { #form {
width: 100%; width: 100%;
} }
#results { .results {
overflow: auto; overflow: auto;
padding: 20px; /* padding: 20px; */
background: lightgray; background: white;
-moz-border-radius: 20px; /* -moz-border-radius: 20px; */
-webkit-border-radius: 20px; /* -webkit-border-radius: 20px; */
-khtml-border-radius: 20px; /* -khtml-border-radius: 20px; */
border-radius: 20px; /* border-radius: 20px; */
} }
#input_request { #input_request {
margin-top: 5px;
display:block; display:block;
width:150px;
word-wrap:break-word; word-wrap:break-word;
white-space:pre;
overflow: auto;
margin-top: 20px;
margin-bottom: 20px;
} }
textarea #input_request a{
{ width:100%;
}
textarea{
border:1px solid #999999; border:1px solid #999999;
width:100%; width:100%;
margin:5px 0; margin:5px 0;
@@ -139,3 +147,8 @@ textarea
#header { #header {
font-family: 'Architects Daughter', cursive; font-family: 'Architects Daughter', cursive;
} }
#results-div {
/* background: white; */
display: none;
}

36377
senpy/static/js/jsoneditor.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -29,45 +29,60 @@ $(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);
var defaultPlugin= JSON.parse($.ajax({type: "GET", url: "/api/plugins/default" , async: false}).responseText); var defaultPlugin= JSON.parse($.ajax({type: "GET", url: "/api/plugins/default" , async: false}).responseText);
html=""; html="";
var availablePlugins = document.getElementById('availablePlugins');
plugins = response.plugins; plugins = response.plugins;
for (r in plugins){ for (r in plugins){
if (plugins[r]["name"]){ plugin = plugins[r]
if (plugins[r]["name"] == defaultPlugin["name"]){ if (plugin["name"]){
if (plugins[r]["is_activated"]){ if (plugin["name"] == defaultPlugin["name"]){
html+= "<option value=\""+plugins[r]["name"]+"\" selected=\"selected\">"+plugins[r]["name"]+"</option>" if (plugin["is_activated"]){
html+= "<option value=\""+plugin["name"]+"\" selected=\"selected\">"+plugin["name"]+"</option>"
}else{ }else{
html+= "<option value=\""+plugins[r]["name"]+"\" selected=\"selected\" disabled=\"disabled\">"+plugins[r]["name"]+"</option>" html+= "<option value=\""+plugin["name"]+"\" selected=\"selected\" disabled=\"disabled\">"+plugin["name"]+"</option>"
} }
} }
else{ else{
if (plugins[r]["is_activated"]){ if (plugin["is_activated"]){
html+= "<option value=\""+plugins[r]["name"]+"\">"+plugins[r]["name"]+"</option>" html+= "<option value=\""+plugin["name"]+"\">"+plugin["name"]+"</option>"
} }
else{ else{
html+= "<option value=\""+plugins[r]["name"]+"\" disabled=\"disabled\">"+plugins[r]["name"]+"</option>" html+= "<option value=\""+plugin["name"]+"\" disabled=\"disabled\">"+plugin["name"]+"</option>"
} }
} }
} }
if (plugins[r]["extra_params"]){ if (plugin["extra_params"]){
plugins_params[plugins[r]["name"]]={}; plugins_params[plugin["name"]]={};
for (param in plugins[r]["extra_params"]){ for (param in plugin["extra_params"]){
if (typeof plugins[r]["extra_params"][param] !="string"){ if (typeof plugin["extra_params"][param] !="string"){
var params = new Array(); var params = new Array();
var alias = plugins[r]["extra_params"][param]["aliases"][0]; var alias = plugin["extra_params"][param]["aliases"][0];
params[alias]=new Array(); params[alias]=new Array();
for (option in plugins[r]["extra_params"][param]["options"]){ for (option in plugin["extra_params"][param]["options"]){
params[alias].push(plugins[r]["extra_params"][param]["options"][option]) params[alias].push(plugin["extra_params"][param]["options"][option])
} }
plugins_params[plugins[r]["name"]][alias] = (params[alias]) plugins_params[plugin["name"]][alias] = (params[alias])
} }
} }
} }
var pluginList = document.createElement('li');
newHtml = ""
if(plugin.url) {
newHtml= "<a href="+plugin.url+">" + plugin.name + "</a>";
}else {
newHtml= plugin["name"];
}
newHtml += ": " + replaceURLWithHTMLLinks(plugin.description);
pluginList.innerHTML = newHtml;
availablePlugins.appendChild(pluginList)
} }
document.getElementById('plugins').innerHTML = html; document.getElementById('plugins').innerHTML = html;
change_params(); change_params();
$(window).on('hashchange', hashchanged); $(window).on('hashchange', hashchanged);
hashchanged(); hashchanged();
$('.tooltip-form').tooltip(); $('.tooltip-form').tooltip();
}); });
@@ -90,6 +105,10 @@ function change_params(){
function load_JSON(){ function load_JSON(){
url = "/api"; url = "/api";
var container = document.getElementById('results');
var rawcontainer = document.getElementById("jsonraw");
rawcontainer.innerHTML = '';
container.innerHTML = '';
var plugin = document.getElementById("plugins").options[document.getElementById("plugins").selectedIndex].value; var plugin = document.getElementById("plugins").options[document.getElementById("plugins").selectedIndex].value;
var input = encodeURIComponent(document.getElementById("input").value); var input = encodeURIComponent(document.getElementById("input").value);
url += "?algo="+plugin+"&i="+input url += "?algo="+plugin+"&i="+input
@@ -102,9 +121,16 @@ function load_JSON(){
} }
} }
var response = JSON.parse($.ajax({type: "GET", url: url , async: false}).responseText); var response = JSON.parse($.ajax({type: "GET", url: url , async: false}).responseText);
document.getElementById("results").innerHTML = replaceURLWithHTMLLinks(JSON.stringify(response, undefined, 2)) var options = {
document.getElementById("input_request").innerHTML = "<label>"+url+"</label>" mode: 'view'
};
var editor = new JSONEditor(container, options, response);
editor.expandAll();
rawcontainer.innerHTML = replaceURLWithHTMLLinks(JSON.stringify(response, undefined, 2))
document.getElementById("input_request").innerHTML = "<a href='"+url+"'>"+url+"</a>"
document.getElementById("results-div").style.display = 'block';
} }

View File

@@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" /> <meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Playground</title> <title>Playground {{version}}</title>
</head> </head>
<script src="static/js/jquery-2.1.1.min.js" ></script> <script src="static/js/jquery-2.1.1.min.js" ></script>
@@ -10,54 +10,83 @@
<link rel="stylesheet" href="static/css/bootstrap.min.css"> <link rel="stylesheet" href="static/css/bootstrap.min.css">
<link rel="stylesheet" href="static/css/main.css"> <link rel="stylesheet" href="static/css/main.css">
<link rel="stylesheet" href="static/font-awesome-4.1.0/css/font-awesome.min.css"> <link rel="stylesheet" href="static/font-awesome-4.1.0/css/font-awesome.min.css">
<link href="static/css/jsoneditor.css" rel="stylesheet" type="text/css">
<script src="static/js/bootstrap.min.js"></script> <script src="static/js/bootstrap.min.js"></script>
<script src="static/js/jsoneditor.js"></script>
<script src="static/js/main.js"></script> <script src="static/js/main.js"></script>
<body> <body>
<div class="container"> <div class="container">
<div id="header"> <div id="header">
<h3 id="header-title"> <h3 id="header-title">
<a href="https://github.com/gsi-upm/senpy" target="_blank"> <a href="https://github.com/gsi-upm/senpy" target="_blank">
<img id="header-logo" class="imsg-responsive" src="static/img/header.png"/></a> Playground <img id="header-logo" class="imsg-responsive" src="static/img/header.png"/></a> Playground
</h3> </h3>
<h4>v{{ version}}</h4>
</div> </div>
<ul class="nav nav-tabs" role="tablist"> <ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a class="active" href="#about">About</a></li> <li role="presentation" ><a class="active" href="#about">About</a></li>
<li role="presentation"><a class="active" href="#test">Test it</a></li> <li role="presentation"class="active"><a class="active" href="#test">Test it</a></li>
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane active" id="about"> <div class="tab-pane" id="about">
<div class="row"> <div class="row">
<div class="col-lg-6 "> <div class="col-lg-6">
<div class="well"> <h2>About Senpy</h2>
<h2>Test Senpy</h2> <p>Senpy is a framework to build semantic sentiment and emotion analysis services. It does so by using a mix of web and semantic technologies, such as JSON-LD, RDFlib and Flask.</p>
<div> <p>Senpy makes it easy to develop and publish your own analysis algorithms (plugins in senpy terms).
<p class="text-center"> </p>
<a class="btn btn-lg btn-primary" href="#test" role="button">Test it »</a> <p>
This website is the senpy Playground, which allows you to test the instance of senpy in this server. It provides a user-friendly interface to the functions exposed by the senpy API.
</p>
<p>
Once you get comfortable with the parameters and results, you are encouraged to issue your own requests to the API endpoint, which should be <a href="/api">here</a>.
</p>
<p>
These are some of the things you can do with the API:
<ul>
<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>Download the JSON-LD context used: <a href="/api/contexts/Results.jsonld">/api/contexts/Results.jsonld</a></li>
</ul>
</p> </p>
</div> </div>
<div class="col-lg-6 ">
<div class="panel panel-default">
<div class="panel-heading">
Available Plugins
</div>
<div class="panel-body"><ul id=availablePlugins></ul></div>
</div> </div>
</div> </div>
<div class="col-lg-6 "> <div class="col-lg-6 ">
<a href="http://senpy.readthedocs.io">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><i class="fa fa-sign-in"></i> Follow us on <a href="http://www.github.com/gsi-upm/senpy">GitHub</a></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>
</div> </div>
</a>
<a href="http://www.github.com/gsi-upm/senpy">
<div class="panel panel-default">
<div class="panel-heading"><i class="fa fa-sign-in"></i> Feel free to follow us on GitHub</div>
</div>
</a>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><i class="fa fa-child"></i> Enjoy.</div> <div class="panel-heading"><i class="fa fa-child"></i> Enjoy.</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane" id="test"> <div class="tab-pane active" id="test">
<div class="well"> <div class="well">
<form id="form" onsubmit="return getPlugins();" accept-charset="utf-8"> <form id="form" onsubmit="return getPlugins();" accept-charset="utf-8">
<div id="inputswrapper"> <div id="inputswrapper">
@@ -71,15 +100,32 @@ I cannot believe it!</textarea></div>
<div id ="params"> <div id ="params">
</div> </div>
</br> </br>
<a id="preview" class="btn btn-lg btn-primary" href="#" onclick="load_JSON()">Analyse!</a> <a id="preview" class="btn btn-lg btn-primary" onclick="load_JSON()">Analyse!</a>
<!--<button id="visualise" name="type" type="button">Visualise!</button>--> <!--<button id="visualise" name="type" type="button">Visualise!</button>-->
</div>
</form> </form>
<div id="content">
<span id="input_request"></span>
<pre id="results"></pre>
</div>
</div>
</div> </div>
<span id="input_request"></span>
<div id="results-div">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a class="active" href="#viewer">Viewer</a></li>
<li role="presentation"><a class="active" href="#raw">Raw</a></li>
</ul>
<div class="tab-content" id="results-container">
<div class="tab-pane active" id="viewer">
<div id="content">
<pre id="results" class="results"></pre>
</div>
</div>
<div class="tab-pane" id="raw">
<div id="content">
<pre id="jsonraw" class="results"></pre>
</div>
</div>
</div>
</div>
</div> </div>
<a href="http://www.gsi.dit.upm.es" target="_blank"><img class="center-block" src="static/img/gsi.png"/> </a> <a href="http://www.gsi.dit.upm.es" target="_blank"><img class="center-block" src="static/img/gsi.png"/> </a>

15
senpy/version.py Normal file
View File

@@ -0,0 +1,15 @@
import os
import logging
logger = logging.getLogger(__name__)
ROOT = os.path.dirname(__file__)
DEFAULT_FILE = os.path.join(ROOT, 'VERSION')
def read_version(versionfile=DEFAULT_FILE):
with open(versionfile) as f:
return f.read().strip()
__version__ = read_version()

View File

@@ -2,3 +2,11 @@
description-file = README.rst description-file = README.rst
[aliases] [aliases]
test=pytest test=pytest
[flake8]
# because of the way that future works, we need to call install_aliases before
# finishing the imports. flake8 thinks that we're doing the imports too late,
# but it's actually ok
ignore = E402
max-line-length = 100
[bdist_wheel]
universal=1

View File

@@ -1,44 +1,41 @@
import pip import pip
from setuptools import setup from setuptools import setup
from pip.req import parse_requirements
# parse_requirements() returns generator of pip.req.InstallRequirement objects # parse_requirements() returns generator of pip.req.InstallRequirement objects
from pip.req import parse_requirements
from senpy import __version__
try: try:
install_reqs = parse_requirements("requirements.txt", session=pip.download.PipSession()) install_reqs = parse_requirements(
test_reqs = parse_requirements("test-requirements.txt", session=pip.download.PipSession()) "requirements.txt", session=pip.download.PipSession())
test_reqs = parse_requirements(
"test-requirements.txt", session=pip.download.PipSession())
except AttributeError: except AttributeError:
install_reqs = parse_requirements("requirements.txt") install_reqs = parse_requirements("requirements.txt")
test_reqs = parse_requirements("test-requirements.txt") test_reqs = parse_requirements("test-requirements.txt")
# reqs is a list of requirement
# e.g. ['django==1.5.1', 'mezzanine==1.4.6']
install_reqs = [str(ir.req) for ir in install_reqs] install_reqs = [str(ir.req) for ir in install_reqs]
test_reqs = [str(ir.req) for ir in test_reqs] test_reqs = [str(ir.req) for ir in test_reqs]
exec(open('senpy/__init__.py').read())
setup( setup(
name='senpy', name='senpy',
packages=['senpy'], # this must be the same as the name above packages=['senpy'], # this must be the same as the name above
version=__version__, version=__version__,
description=''' description=('A sentiment analysis server implementation. '
A sentiment analysis server implementation. Designed to be \ 'Designed to be extensible, so new algorithms '
extendable, so new algorithms and sources can be used. 'and sources can be used.'),
''',
author='J. Fernando Sanchez', author='J. Fernando Sanchez',
author_email='balkian@gmail.com', author_email='balkian@gmail.com',
url='https://github.com/gsi-upm/senpy', # use the URL to the github repo url='https://github.com/gsi-upm/senpy', # use the URL to the github repo
download_url='https://github.com/gsi-upm/senpy/archive/{}.tar.gz' .format(__version__), download_url='https://github.com/gsi-upm/senpy/archive/{}.tar.gz'.format(
__version__),
keywords=['eurosentiment', 'sentiment', 'emotions', 'nif'], keywords=['eurosentiment', 'sentiment', 'emotions', 'nif'],
classifiers=[], classifiers=[],
install_requires=install_reqs, install_requires=install_reqs,
tests_require=test_reqs, tests_require=test_reqs,
setup_requires=['pytest-runner',], setup_requires=['pytest-runner', ],
include_package_data=True, include_package_data=True,
entry_points={ entry_points={
'console_scripts': [ 'console_scripts':
'senpy = senpy.__main__:main', ['senpy = senpy.__main__:main', 'senpy-cli = senpy.cli:main']
'senpy-cli = senpy.cli:main' })
]
}
)

View File

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

View File

@@ -1,8 +0,0 @@
from senpy.plugins import SentimentPlugin
from senpy.models import Results
class DummyPlugin(SentimentPlugin):
def analyse(self, *args, **kwargs):
return Results()

View File

@@ -1,7 +0,0 @@
{
"name": "Dummy",
"module": "dummy",
"description": "I am dummy",
"author": "@balkian",
"version": "0.1"
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,9 +2,9 @@ import os
import logging import logging
from senpy.extensions import Senpy from senpy.extensions import Senpy
from senpy import models
from flask import Flask from flask import Flask
from flask.ext.testing import TestCase from unittest import TestCase
from gevent import sleep
from itertools import product from itertools import product
@@ -12,30 +12,39 @@ def check_dict(indic, template):
return all(item in indic.items() for item in template.items()) return all(item in indic.items() for item in template.items())
class BlueprintsTest(TestCase): def parse_resp(resp):
return models.from_json(resp.data.decode('utf-8'))
def create_app(self):
class BlueprintsTest(TestCase):
def setUp(self):
self.app = Flask("test_extensions") self.app = Flask("test_extensions")
self.client = self.app.test_client()
self.senpy = Senpy() self.senpy = Senpy()
self.senpy.init_app(self.app) self.senpy.init_app(self.app)
self.dir = os.path.join(os.path.dirname(__file__), "..") self.dir = os.path.join(os.path.dirname(__file__), "..")
self.senpy.add_folder(self.dir) self.senpy.add_folder(self.dir)
self.senpy.activate_plugin("Dummy", sync=True) self.senpy.activate_plugin("Dummy", sync=True)
return self.app self.senpy.activate_plugin("DummyRequired", sync=True)
self.senpy.default_plugin = 'Dummy'
def assertCode(self, resp, code):
self.assertEqual(resp.status_code, code)
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
""" """
resp = self.client.get("/api/") resp = self.client.get("/api/")
self.assert404(resp) self.assertCode(resp, 400)
logging.debug(resp.json) js = parse_resp(resp)
assert resp.json["status"] == 404 logging.debug(js)
assert js["status"] == 400
atleast = { atleast = {
"status": 404, "status": 400,
"message": "Missing or invalid parameters", "message": "Missing or invalid parameters",
} }
assert check_dict(resp.json, atleast) assert check_dict(js, atleast)
def test_analysis(self): def test_analysis(self):
""" """
@@ -43,81 +52,102 @@ class BlueprintsTest(TestCase):
it should contain the context it should contain the context
""" """
resp = self.client.get("/api/?i=My aloha mohame") resp = self.client.get("/api/?i=My aloha mohame")
self.assert200(resp) self.assertCode(resp, 200)
logging.debug("Got response: %s", resp.json) js = parse_resp(resp)
assert "@context" in resp.json logging.debug("Got response: %s", js)
assert "entries" in resp.json assert "@context" in js
assert "entries" in js
def test_analysis_extra(self):
"""
Extra params that have a default should
"""
resp = self.client.get("/api/?i=My aloha mohame&algo=Dummy")
self.assertCode(resp, 200)
js = parse_resp(resp)
logging.debug("Got response: %s", js)
assert "@context" in js
assert "entries" in js
def test_analysis_extra_required(self):
"""
Extra params that have a required argument that does not
have a default should raise an error.
"""
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired")
self.assertCode(resp, 400)
js = parse_resp(resp)
logging.debug("Got response: %s", js)
assert isinstance(js, models.Error)
def test_error(self):
"""
The dummy plugin returns an empty response,\
it should contain the context
"""
resp = self.client.get("/api/?i=My aloha mohame&algo=DOESNOTEXIST")
self.assertCode(resp, 404)
js = parse_resp(resp)
logging.debug("Got response: %s", js)
assert isinstance(js, models.Error)
def test_list(self): def test_list(self):
""" List the plugins """ """ List the plugins """
resp = self.client.get("/api/plugins/") resp = self.client.get("/api/plugins/")
self.assert200(resp) self.assertCode(resp, 200)
logging.debug(resp.json) js = parse_resp(resp)
assert 'plugins' in resp.json logging.debug(js)
plugins = resp.json['plugins'] assert 'plugins' in js
plugins = js['plugins']
assert len(plugins) > 1 assert len(plugins) > 1
assert list(p for p in plugins if p['name'] == "Dummy") assert list(p for p in plugins if p['name'] == "Dummy")
assert "@context" in resp.json assert "@context" in js
def test_headers(self): def test_headers(self):
for i, j in product(["/api/plugins/?nothing=", "/api/?i=test&"], for i, j in product(["/api/plugins/?nothing=", "/api/?i=test&"],
["inHeaders"]): ["inHeaders"]):
resp = self.client.get("%s" % (i)) resp = self.client.get("%s" % (i))
assert "@context" in resp.json js = parse_resp(resp)
assert "@context" in js
resp = self.client.get("%s&%s=0" % (i, j)) resp = self.client.get("%s&%s=0" % (i, j))
assert "@context" in resp.json js = parse_resp(resp)
assert "@context" in js
resp = self.client.get("%s&%s=1" % (i, j)) resp = self.client.get("%s&%s=1" % (i, j))
assert "@context" not in resp.json js = parse_resp(resp)
assert "@context" not in js
resp = self.client.get("%s&%s=true" % (i, j)) resp = self.client.get("%s&%s=true" % (i, j))
assert "@context" not in resp.json js = parse_resp(resp)
assert "@context" not in js
def test_detail(self): def test_detail(self):
""" Show only one plugin""" """ Show only one plugin"""
resp = self.client.get("/api/plugins/Dummy/") resp = self.client.get("/api/plugins/Dummy/")
self.assert200(resp) self.assertCode(resp, 200)
logging.debug(resp.json) js = parse_resp(resp)
assert "@id" in resp.json logging.debug(js)
assert resp.json["@id"] == "Dummy_0.1" assert "@id" in js
assert js["@id"] == "plugins/Dummy_0.1"
def test_activate(self):
""" Activate and deactivate one plugin """
resp = self.client.get("/api/plugins/Dummy/deactivate")
self.assert200(resp)
sleep(0.5)
resp = self.client.get("/api/plugins/Dummy/")
self.assert200(resp)
assert "is_activated" in resp.json
assert resp.json["is_activated"] == False
resp = self.client.get("/api/plugins/Dummy/activate")
self.assert200(resp)
sleep(0.5)
resp = self.client.get("/api/plugins/Dummy/")
self.assert200(resp)
assert "is_activated" in resp.json
assert resp.json["is_activated"] == True
def test_default(self): def test_default(self):
""" Show only one plugin""" """ Show only one plugin"""
resp = self.client.get("/api/plugins/default/") resp = self.client.get("/api/plugins/default/")
self.assert200(resp) self.assertCode(resp, 200)
logging.debug(resp.json) js = parse_resp(resp)
assert "@id" in resp.json logging.debug(js)
assert resp.json["@id"] == "Dummy_0.1" assert "@id" in js
resp = self.client.get("/api/plugins/Dummy/deactivate") assert js["@id"] == "plugins/Dummy_0.1"
self.assert200(resp)
sleep(0.5)
resp = self.client.get("/api/plugins/default/")
self.assert404(resp)
def test_context(self): def test_context(self):
resp = self.client.get("/api/contexts/context.jsonld") resp = self.client.get("/api/contexts/context.jsonld")
self.assert200(resp) self.assertCode(resp, 200)
assert "@context" in resp.json js = parse_resp(resp)
assert "@context" in js
assert check_dict( assert check_dict(
resp.json["@context"], js["@context"],
{"marl": "http://www.gsi.dit.upm.es/ontologies/marl/ns#"}) {"marl": "http://www.gsi.dit.upm.es/ontologies/marl/ns#"})
def test_schema(self): def test_schema(self):
resp = self.client.get("/api/schemas/definitions.json") resp = self.client.get("/api/schemas/definitions.json")
self.assert200(resp) self.assertCode(resp, 200)
assert "$schema" in resp.json js = parse_resp(resp)
assert "$schema" in js

View File

@@ -1,7 +1,11 @@
import os
import logging import logging
from functools import partial from functools import partial
try:
from unittest.mock import patch
except ImportError:
from mock import patch
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from unittest import TestCase from unittest import TestCase
@@ -10,12 +14,14 @@ from senpy.models import Error
class CLITest(TestCase): class CLITest(TestCase):
def test_basic(self): def test_basic(self):
self.assertRaises(Error, partial(main_function, [])) self.assertRaises(Error, partial(main_function, []))
res = main_function(['--input', 'test'])
assert 'entries' in res with patch('senpy.extensions.Senpy.analyse') as patched:
res = main_function(['--input', 'test', '--algo', 'rand']) main_function(['--input', 'test'])
assert 'entries' in res
assert 'analysis' in res patched.assert_called_with(input='test')
assert res['analysis'][0]['name'] == 'rand' with patch('senpy.extensions.Senpy.analyse') as patched:
main_function(['--input', 'test', '--algo', 'rand'])
patched.assert_called_with(input='test', algo='rand')

43
tests/test_client.py Normal file
View File

@@ -0,0 +1,43 @@
from unittest import TestCase
try:
from unittest.mock import patch
except ImportError:
from mock import patch
from senpy.client import Client
from senpy.models import Results, Error
class Call(dict):
def __init__(self, obj):
self.obj = obj.jsonld()
self.status_code = 200
self.content = self.json()
def json(self):
return self.obj
class ModelsTest(TestCase):
def setUp(self):
self.host = '0.0.0.0'
self.port = 5000
def test_client(self):
endpoint = 'http://dummy/'
client = Client(endpoint)
success = Call(Results())
with patch('requests.request', return_value=success) as patched:
resp = client.analyse('hello')
assert isinstance(resp, Results)
patched.assert_called_with(
url=endpoint + '/', method='GET', params={'input': 'hello'})
error = Call(Error('Nothing'))
with patch('requests.request', return_value=error) as patched:
resp = client.analyse(input='hello', algorithm='NONEXISTENT')
assert isinstance(resp, Error)
patched.assert_called_with(
url=endpoint + '/',
method='GET',
params={'input': 'hello',
'algorithm': 'NONEXISTENT'})

View File

@@ -2,22 +2,26 @@ from __future__ import print_function
import os import os
import logging import logging
from functools import partial try:
from unittest import mock
except ImportError:
import mock
from functools import partial
from senpy.extensions import Senpy from senpy.extensions import Senpy
from senpy.models import Error from senpy.models import Error
from flask import Flask from flask import Flask
from flask.ext.testing import TestCase from unittest import TestCase
class ExtensionsTest(TestCase): class ExtensionsTest(TestCase):
def setUp(self):
def create_app(self): self.app = Flask('test_extensions')
self.app = Flask("test_extensions")
self.dir = os.path.join(os.path.dirname(__file__)) self.dir = os.path.join(os.path.dirname(__file__))
self.senpy = Senpy(plugin_folder=self.dir, default_plugins=False) self.senpy = Senpy(plugin_folder=self.dir,
self.senpy.init_app(self.app) app=self.app,
default_plugins=False)
self.senpy.activate_plugin("Dummy", sync=True) self.senpy.activate_plugin("Dummy", sync=True)
return self.app
def test_init(self): def test_init(self):
""" Initialising the app with the extension. """ """ Initialising the app with the extension. """
@@ -34,9 +38,24 @@ class ExtensionsTest(TestCase):
assert "Dummy" in self.senpy.plugins assert "Dummy" in self.senpy.plugins
def test_enabling(self): def test_enabling(self):
""" Enabling a plugin """
info = {
'name': 'TestPip',
'module': 'dummy',
'requirements': ['noop'],
'version': 0
}
root = os.path.join(self.dir, 'plugins', 'dummy_plugin')
name, module = self.senpy._load_plugin_from_info(info, root=root)
assert name == 'TestPip'
assert module
import noop
dir(noop)
def test_installing(self):
""" Enabling a plugin """ """ Enabling a plugin """
self.senpy.activate_all(sync=True) self.senpy.activate_all(sync=True)
assert len(self.senpy.plugins) == 2 assert len(self.senpy.plugins) >= 3
assert self.senpy.plugins["Sleep"].is_activated assert self.senpy.plugins["Sleep"].is_activated
def test_disabling(self): def test_disabling(self):
@@ -57,6 +76,11 @@ class ExtensionsTest(TestCase):
""" Don't analyse if there isn't any plugin installed """ """ Don't analyse if there isn't any plugin installed """
self.senpy.deactivate_all(sync=True) self.senpy.deactivate_all(sync=True)
self.assertRaises(Error, partial(self.senpy.analyse, input="tupni")) self.assertRaises(Error, partial(self.senpy.analyse, input="tupni"))
self.assertRaises(Error,
partial(
self.senpy.analyse,
input="tupni",
algorithm='Dummy'))
def test_analyse(self): def test_analyse(self):
""" Using a plugin """ """ Using a plugin """
@@ -65,8 +89,23 @@ class ExtensionsTest(TestCase):
r1 = self.senpy.analyse( r1 = self.senpy.analyse(
algorithm="Dummy", input="tupni", output="tuptuo") algorithm="Dummy", input="tupni", output="tuptuo")
r2 = self.senpy.analyse(input="tupni", output="tuptuo") r2 = self.senpy.analyse(input="tupni", output="tuptuo")
assert r1.analysis[0].id[:5] == "Dummy" assert r1.analysis[0] == "plugins/Dummy_0.1"
assert r2.analysis[0].id[:5] == "Dummy" assert r2.analysis[0] == "plugins/Dummy_0.1"
assert r1.entries[0].text == 'input'
def test_analyse_error(self):
mm = mock.MagicMock()
mm.analyse_entry.side_effect = Error('error on analysis', status=900)
self.senpy.plugins['MOCK'] = mm
resp = self.senpy.analyse(input='nothing', algorithm='MOCK')
assert resp['message'] == 'error on analysis'
assert resp['status'] == 900
mm.analyse.side_effect = Exception('generic exception on analysis')
mm.analyse_entry.side_effect = Exception(
'generic exception on analysis')
resp = self.senpy.analyse(input='nothing', algorithm='MOCK')
assert resp['message'] == 'generic exception on analysis'
assert resp['status'] == 500
def test_filtering(self): def test_filtering(self):
""" Filtering plugins """ """ Filtering plugins """
@@ -76,3 +115,7 @@ class ExtensionsTest(TestCase):
self.senpy.deactivate_plugin("Dummy", sync=True) self.senpy.deactivate_plugin("Dummy", sync=True)
assert not len( assert not len(
self.senpy.filter_plugins(name="Dummy", is_activated=True)) self.senpy.filter_plugins(name="Dummy", is_activated=True))
def test_load_default_plugins(self):
senpy = Senpy(plugin_folder=self.dir, default_plugins=True)
assert len(senpy.plugins) > 1

View File

@@ -1,54 +1,56 @@
import os
import logging import logging
import jsonschema import jsonschema
import json import json
import os import rdflib
from unittest import TestCase from unittest import TestCase
from senpy.models import Response, Entry, Results, Sentiment, EmotionSet, Emotion, Error from senpy.models import (Emotion,
EmotionAnalysis,
EmotionSet,
Entry,
Error,
Results,
Sentiment)
from senpy.plugins import SenpyPlugin from senpy.plugins import SenpyPlugin
from pprint import pprint from pprint import pprint
class ModelsTest(TestCase): class ModelsTest(TestCase):
def test_jsonld(self): def test_jsonld(self):
ctx = os.path.normpath(os.path.join(__file__, "..", "..", "..", "senpy", "schemas", "context.jsonld")) prueba = {"id": "test", "analysis": [], "entries": []}
prueba = {"id": "test",
"analysis": [],
"entries": []}
r = Results(**prueba) r = Results(**prueba)
print("Response's context: ") print("Response's context: ")
pprint(r.context) pprint(r._context)
assert r.id == "test" assert r.id == "test"
j = r.jsonld(with_context=True) j = r.jsonld(with_context=True)
print("As JSON:") print("As JSON:")
pprint(j) pprint(j)
assert("@context" in j) assert ("@context" in j)
assert("marl" in j["@context"]) assert ("marl" in j["@context"])
assert("entries" in j["@context"]) assert ("entries" in j["@context"])
assert(j["@id"] == "test") assert (j["@id"] == "test")
assert "id" not in j assert "id" not in j
r6 = Results(**prueba) r6 = Results(**prueba)
r6.entries.append(Entry({"@id":"ohno", "nif:isString":"Just testing"})) e = Entry({"@id": "ohno", "nif:isString": "Just testing"})
r6.entries.append(e)
logging.debug("Reponse 6: %s", r6) logging.debug("Reponse 6: %s", r6)
assert("marl" in r6.context) assert ("marl" in r6._context)
assert("entries" in r6.context) assert ("entries" in r6._context)
j6 = r6.jsonld(with_context=True) j6 = r6.jsonld(with_context=True)
logging.debug("jsonld: %s", j6) logging.debug("jsonld: %s", j6)
assert("@context" in j6) assert ("@context" in j6)
assert("entries" in j6) assert ("entries" in j6)
assert("analysis" in j6) assert ("analysis" in j6)
resp = r6.flask() resp = r6.flask()
received = json.loads(resp.data.decode()) received = json.loads(resp.data.decode())
logging.debug("Response: %s", j6) logging.debug("Response: %s", j6)
assert(received["entries"]) assert (received["entries"])
assert(received["entries"][0]["nif:isString"] == "Just testing") assert (received["entries"][0]["nif:isString"] == "Just testing")
assert(received["entries"][0]["nif:isString"] != "Not testing") assert (received["entries"][0]["nif:isString"] != "Not testing")
def test_id(self): def test_id(self):
''' Adding the id after creation should overwrite the automatic ID ''' Adding the id after creation should overwrite the automatic ID
@@ -61,7 +63,6 @@ class ModelsTest(TestCase):
assert j2['@id'] == 'test' assert j2['@id'] == 'test'
assert 'id' not in j2 assert 'id' not in j2
def test_entries(self): def test_entries(self):
e = Entry() e = Entry()
self.assertRaises(jsonschema.ValidationError, e.validate) self.assertRaises(jsonschema.ValidationError, e.validate)
@@ -103,5 +104,46 @@ class ModelsTest(TestCase):
logging.debug(c) logging.debug(c)
p.validate() p.validate()
def test_frame_response(self): def test_str(self):
"""The string representation shouldn't include private variables"""
r = Results()
p = SenpyPlugin({"name": "STR test", "version": 0})
p._testing = 0
s = str(p)
assert "_testing" not in s
r.analysis.append(p)
s = str(r)
assert "_testing" not in s
def test_turtle(self):
"""Any model should be serializable as a turtle file"""
ana = EmotionAnalysis()
res = Results()
res.analysis.append(ana)
entry = Entry(text='Just testing')
eSet = EmotionSet()
emotion = Emotion()
entry.emotions.append(eSet)
res.entries.append(entry)
eSet.onyx__hasEmotion.append(emotion)
eSet.prov__wasGeneratedBy = ana.id
triples = ('ana a :Analysis',
'entry a :entry',
' nif:isString "Just testing"',
' onyx:hasEmotionSet eSet',
'eSet a onyx:EmotionSet',
' prov:wasGeneratedBy ana',
' onyx:hasEmotion emotion',
'emotion a onyx:Emotion',
'res a :results',
' me:AnalysisInvoloved ana',
' prov:used entry')
t = res.serialize(format='turtle')
print(t)
g = rdflib.Graph().parse(data=t, format='turtle')
assert len(g) == len(triples)
def test_convert_emotions(self):
"""It should be possible to convert between different emotion models"""
pass pass

View File

@@ -1,31 +1,34 @@
#!/bin/env python #!/bin/env python
import os import os
import logging
import pickle import pickle
import shutil import shutil
import tempfile import tempfile
import json
import os
from unittest import TestCase from unittest import TestCase
from senpy.models import Results, Entry from senpy.models import Results, Entry
from senpy.plugins import SenpyPlugin, ShelfMixin from senpy.plugins import SentimentPlugin, ShelfMixin
class ShelfTest(ShelfMixin, SenpyPlugin): class ShelfDummyPlugin(SentimentPlugin, ShelfMixin):
def activate(self, *args, **kwargs):
if 'counter' not in self.sh:
self.sh['counter'] = 0
self.save()
def test(self, key=None, value=None): def deactivate(self, *args, **kwargs):
assert key in self.sh self.save()
print('Checking: sh[{}] == {}'.format(key, value))
print('SH[{}]: {}'.format(key, self.sh[key])) def analyse(self, *args, **kwargs):
assert self.sh[key] == value self.sh['counter'] = self.sh['counter'] + 1
e = Entry()
e.nif__isString = self.sh['counter']
r = Results()
class ModelsTest(TestCase): r.entries.append(e)
return r
class PluginsTest(TestCase):
def tearDown(self): def tearDown(self):
if os.path.exists(self.shelf_dir): if os.path.exists(self.shelf_dir):
shutil.rmtree(self.shelf_dir) shutil.rmtree(self.shelf_dir)
@@ -36,17 +39,29 @@ class ModelsTest(TestCase):
def setUp(self): def setUp(self):
self.shelf_dir = tempfile.mkdtemp() self.shelf_dir = tempfile.mkdtemp()
self.shelf_file = os.path.join(self.shelf_dir, "shelf") self.shelf_file = os.path.join(self.shelf_dir, "shelf")
def test_shelf_file(self):
a = ShelfDummyPlugin(
info={'name': 'default_shelve_file',
'version': 'test'})
a.activate()
assert os.path.isfile(a.shelf_file)
os.remove(a.shelf_file)
def test_shelf(self): def test_shelf(self):
''' A shelf is created and the value is stored ''' ''' A shelf is created and the value is stored '''
a = ShelfTest(info={'name': 'shelve', a = ShelfDummyPlugin(info={
'version': 'test', 'name': 'shelve',
'shelf_file': self.shelf_file}) 'version': 'test',
'shelf_file': self.shelf_file
})
assert a.sh == {} assert a.sh == {}
a.activate()
assert a.sh == {'counter': 0}
assert a.shelf_file == self.shelf_file assert a.shelf_file == self.shelf_file
a.sh['a'] = 'fromA' a.sh['a'] = 'fromA'
a.test(key='a', value='fromA') assert a.sh['a'] == 'fromA'
a.save() a.save()
@@ -54,19 +69,54 @@ class ModelsTest(TestCase):
assert sh['a'] == 'fromA' assert sh['a'] == 'fromA'
def test_dummy_shelf(self):
a = ShelfDummyPlugin(info={
'name': 'DummyShelf',
'shelf_file': self.shelf_file,
'version': 'test'
})
a.activate()
assert a.shelf_file == self.shelf_file
res1 = a.analyse(input=1)
assert res1.entries[0].nif__isString == 1
res2 = a.analyse(input=1)
assert res2.entries[0].nif__isString == 2
def test_two(self): def test_two(self):
''' Reusing the values of a previous shelf ''' ''' Reusing the values of a previous shelf '''
a = ShelfTest(info={'name': 'shelve', a = ShelfDummyPlugin(info={
'version': 'test', 'name': 'shelve',
'shelf_file': self.shelf_file}) 'version': 'test',
'shelf_file': self.shelf_file
})
a.activate()
print('Shelf file: %s' % a.shelf_file) print('Shelf file: %s' % a.shelf_file)
a.sh['a'] = 'fromA' a.sh['a'] = 'fromA'
a.save() a.save()
b = ShelfTest(info={'name': 'shelve', b = ShelfDummyPlugin(info={
'version': 'test', 'name': 'shelve',
'shelf_file': self.shelf_file}) 'version': 'test',
b.test(key='a', value='fromA') 'shelf_file': self.shelf_file
})
b.activate()
assert b.sh['a'] == 'fromA'
b.sh['a'] = 'fromB' b.sh['a'] = 'fromB'
assert b.sh['a'] == 'fromB' assert b.sh['a'] == 'fromB'
def test_extra_params(self):
''' Should be able to set extra parameters'''
a = ShelfDummyPlugin(info={
'name': 'shelve',
'version': 'test',
'shelf_file': self.shelf_file,
'extra_params': {
'example': {
'aliases': ['example', 'ex'],
'required': True,
'default': 'nonsense'
}
}
})
assert 'example' in a.extra_params

60
tests/test_schemas.py Normal file
View File

@@ -0,0 +1,60 @@
from __future__ import print_function
import json
import unittest
import os
from os import path
from fnmatch import fnmatch
from jsonschema import RefResolver, Draft4Validator, ValidationError
root_path = path.join(path.dirname(path.realpath(__file__)), '..')
schema_folder = path.join(root_path, 'senpy', 'schemas')
examples_path = path.join(root_path, 'docs', 'examples')
bad_examples_path = path.join(root_path, 'docs', 'bad-examples')
class JSONSchemaTests(unittest.TestCase):
pass
def do_create_(jsfile, success):
def do_expected(self):
with open(jsfile) as f:
js = json.load(f)
try:
assert '@type' in js
schema_name = js['@type']
with open(os.path.join(schema_folder, schema_name +
".json")) as file_object:
schema = json.load(file_object)
resolver = RefResolver('file://' + schema_folder + '/', schema)
validator = Draft4Validator(schema, resolver=resolver)
validator.validate(js)
except (AssertionError, ValidationError, KeyError) as ex:
if success:
raise
return do_expected
def add_examples(dirname, success):
for dirpath, dirnames, filenames in os.walk(dirname):
for i in filenames:
if fnmatch(i, '*.json'):
filename = path.join(dirpath, i)
test_method = do_create_(filename, success)
test_method.__name__ = 'test_file_%s_success_%s' % (filename,
success)
test_method.__doc__ = '%s should %svalidate' % (filename, ''
if success else
'not')
setattr(JSONSchemaTests, test_method.__name__, test_method)
del test_method
add_examples(examples_path, True)
add_examples(bad_examples_path, False)
if __name__ == '__main__':
unittest.main()

2
version.sh Executable file
View File

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