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

Compare commits

..

41 Commits
nacho ... 0.7.1

Author SHA1 Message Date
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
81 changed files with 39175 additions and 639 deletions

73
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,73 @@
image: docker:latest
# When using dind, it's wise to use the overlayfs driver for
# improved performance.
variables:
DOCKER_DRIVER: overlay
DOCKERFILE: Dockerfile
stages:
- test
- images
- release
.test: &test_definition
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.eggs"
cache:
paths:
- .eggs/
key: "$CI_PROJECT_NAME"
stage: test
script:
- python setup.py test
test-3.5:
<<: *test_definition
image: "python:3.5"
test-3.4:
<<: *test_definition
image: "python:3.4"
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

9
Dockerfile-2.7 Normal file
View File

@@ -0,0 +1,9 @@
from python:2.7
WORKDIR /usr/src/app
ADD requirements.txt /usr/src/app/
RUN pip install -r requirements.txt
ADD . /usr/src/app/
RUN pip install .
ENTRYPOINT ["python", "-m", "senpy", "-f", ".", "--host", "0.0.0.0"]

9
Dockerfile-3.4 Normal file
View File

@@ -0,0 +1,9 @@
from python:3.4
WORKDIR /usr/src/app
ADD requirements.txt /usr/src/app/
RUN pip install -r requirements.txt
ADD . /usr/src/app/
RUN pip install .
ENTRYPOINT ["python", "-m", "senpy", "-f", ".", "--host", "0.0.0.0"]

9
Dockerfile-3.5 Normal file
View File

@@ -0,0 +1,9 @@
from python:3.5
WORKDIR /usr/src/app
ADD requirements.txt /usr/src/app/
RUN pip install -r requirements.txt
ADD . /usr/src/app/
RUN pip install .
ENTRYPOINT ["python", "-m", "senpy", "-f", ".", "--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"]

9
Dockerfile.template Normal file
View File

@@ -0,0 +1,9 @@
from python:{{PYVERSION}}
WORKDIR /usr/src/app
ADD requirements.txt /usr/src/app/
RUN pip install -r requirements.txt
ADD . /usr/src/app/
RUN pip install .
ENTRYPOINT ["python", "-m", "senpy", "-f", ".", "--host", "0.0.0.0"]

View File

@@ -1,7 +1,7 @@
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

83
Makefile Normal file
View File

@@ -0,0 +1,83 @@
PYVERSIONS=3.5 3.4 2.7
PYMAIN=$(firstword $(PYVERSIONS))
NAME=senpy
REPO=gsiupm
VERSION=$(shell cat $(NAME)/VERSION)
TARNAME=$(NAME)-$(subst -,.,$(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
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 && docker attach $(NAME)-debug) || docker run -w /usr/src/app/ -v $$PWD:/usr/src/app --entrypoint=/bin/bash -ti --name $(NAME)-debug '$(IMAGENAME)-python$*'
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[1] != "${VERSION}"){ print $$1;}}' | xargs docker rm 2>/dev/null|| true
@docker images | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[1] != "${VERSION}"){ print $$1":"$$2;}}' | xargs docker rmi 2>/dev/null|| true
@docker rmi $(NAME)-debug 2>/dev/null || true
upload_git:
git commit -a
git tag ${VERSION}
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)'
.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",

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",

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

@@ -2,6 +2,8 @@ Developing new plugins
---------------------- ----------------------
Each plugin represents a different analysis process.There are two types of files that are needed by senpy for loading a plugin: Each plugin represents a different analysis process.There are two types of files that are needed by senpy for loading a plugin:
Plugins Interface
=======
- Definition file, has the ".senpy" extension. - Definition file, has the ".senpy" extension.
- Code file, is a python file. - Code file, is a python file.
@@ -34,7 +36,7 @@ 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: called in every user requests. It takes in the parameters supplied by a user and should return a senpy Response.
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.

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

@@ -4,8 +4,9 @@ requests>=2.4.1
GitPython>=0.3.2.RC1 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
semver

1
senpy/VERSION Normal file
View File

@@ -0,0 +1 @@
0.7.1

View File

@@ -17,5 +17,24 @@
""" """
Sentiment analysis server in Python Sentiment analysis server in Python
""" """
from __future__ import print_function
from .version import __version__
__version__ = "0.5.5" try:
import semver
__version_info__ = semver.parse_version_info(__version__)
if __version_info__.prerelease:
import logging
logger = logging.getLogger(__name__)
msg = 'WARNING: You are using a pre-release version of {} ({})'.format(
__name__, __version__)
if len(logging.root.handlers) > 0:
logger.info(msg)
else:
import sys
print(msg, file=sys.stderr)
except ImportError:
print('semver not installed. Not doing version checking')
__all__ = ['api', 'blueprints', 'cli', 'extensions', 'models', 'plugins']

View File

@@ -24,7 +24,6 @@ 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 argparse import argparse
@@ -34,37 +33,51 @@ patch_all(thread=False)
SERVER_PORT = os.environ.get("PORT", 5000) SERVER_PORT = os.environ.get("PORT", 5000)
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="127.0.0.1",
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()
@@ -72,6 +85,9 @@ def main():
app = Flask(__name__) app = Flask(__name__)
app.debug = args.debug app.debug = args.debug
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 +96,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,9 +1,8 @@
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"],
@@ -25,7 +24,7 @@ CLI_PARAMS = {
"required": True, "required": True,
"default": "." "default": "."
}, },
} }
NIF_PARAMS = { NIF_PARAMS = {
"input": { "input": {
@@ -96,10 +95,11 @@ 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, message = Error(
message="Missing or invalid parameters", status=404,
parameters=outdict, message="Missing or invalid parameters",
errors={param: error for param, error in parameters=outdict,
iteritems(wrong_params)}) errors={param: error
for param, error in iteritems(wrong_params)})
raise message raise message
return outdict return outdict

View File

@@ -17,12 +17,12 @@
""" """
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, parse_params
from functools import wraps from functools import wraps
import json
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -30,6 +30,7 @@ logger = logging.getLogger(__name__)
api_blueprint = Blueprint("api", __name__) api_blueprint = Blueprint("api", __name__)
demo_blueprint = Blueprint("demo", __name__) demo_blueprint = Blueprint("demo", __name__)
def get_params(req): def get_params(req):
if req.method == 'POST': if req.method == 'POST':
indict = req.form.to_dict(flat=True) indict = req.form.to_dict(flat=True)
@@ -44,17 +45,20 @@ def get_params(req):
def index(): def index():
return render_template("index.html") return render_template("index.html")
@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}) return jsonify({"@context": Response.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):
@@ -73,12 +77,15 @@ def basic_api(f):
response = ex response = ex
in_headers = web_params["inHeaders"] != "0" in_headers = web_params["inHeaders"] != "0"
headers = {'X-ORIGINAL-PARAMS': raw_params} headers = {'X-ORIGINAL-PARAMS': raw_params}
return response.flask(in_headers=in_headers, return response.flask(
headers=headers, in_headers=in_headers,
context_uri=url_for('api.context', entity=type(response).__name__, headers=headers,
_external=True)) context_uri=url_for(
'api.context', entity=type(response).__name__, _external=True))
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():
@@ -93,13 +100,12 @@ def plugins():
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']) @api_blueprint.route('/plugins/<plugin>/<action>', methods=['POST', 'GET'])
@basic_api @basic_api
def plugin(plugin=None, action="list"): def plugin(plugin=None, action="list"):
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 response = sp.default_plugin
plugin = response.name plugin = response.name
@@ -110,15 +116,8 @@ def plugin(plugin=None, action="list"):
if action == "list": if action == "list":
return response return response
method = "{}_plugin".format(action) method = "{}_plugin".format(action)
if(hasattr(sp, method)): if (hasattr(sp, method)):
getattr(sp, method)(plugin) getattr(sp, method)(plugin)
return Response(message="Ok") return Response(message="Ok")
else: else:
return Error(message="action '{}' not allowed".format(action)) 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)
''' '''
@@ -47,4 +50,3 @@ def main():
if __name__ == '__main__': if __name__ == '__main__':
main() main()

45
senpy/client.py Normal file
View File

@@ -0,0 +1,45 @@
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
if __name__ == '__main__':
c = Client('http://senpy.cluster.gsi.dit.upm.es/api/')
resp = c.analyse('hello')
# print(resp)
print(resp.entries)
resp.validate()

View File

@@ -2,17 +2,15 @@
""" """
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
from .models import Error from .models import Error
from .blueprints import api_blueprint, demo_blueprint from .blueprints import api_blueprint, demo_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 git import Repo, InvalidGitRepositoryError
from functools import partial
from threading import Thread
import os import os
import fnmatch import fnmatch
@@ -21,17 +19,19 @@ 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="plugins",
default_plugins=False):
self.app = app self.app = app
self._search_folders = set() self._search_folders = set()
@@ -79,22 +79,24 @@ class Senpy(object):
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 '{}'"
" is not activated yet").format(algo))
plug = self.plugins[algo] plug = self.plugins[algo]
nif_params = parse_params(params, spec=NIF_PARAMS) nif_params = parse_params(params, spec=NIF_PARAMS)
extra_params = plug.get('extra_params', {}) extra_params = plug.get('extra_params', {})
@@ -119,9 +121,8 @@ class Senpy(object):
return None return None
def parameters(self, algo): def parameters(self, algo):
return getattr(self.plugins.get(algo) or self.default_plugin, return getattr(
"extra_params", self.plugins.get(algo) or self.default_plugin, "extra_params", {})
{})
def activate_all(self, sync=False): def activate_all(self, sync=False):
ps = [] ps = []
@@ -145,47 +146,53 @@ 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("Error deactivating plugin {}: {}".format(
ex)) 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): def reload_plugin(self, name):
logger.debug("Reloading {}".format(name)) logger.debug("Reloading {}".format(name))
@@ -198,43 +205,71 @@ class Senpy(object):
logger.error('Error reloading {}: {}'.format(name, ex)) logger.error('Error reloading {}: {}'.format(name, ex))
self.plugins[name] = plugin self.plugins[name] = plugin
@staticmethod @classmethod
def _load_plugin(root, filename): def validate_info(cls, info):
logger.debug("Loading plugin: {}".format(filename)) return all(x in info for x in ('name', 'module', 'version'))
fpath = os.path.join(root, filename)
with open(fpath, 'r') as f: def install_deps(self):
info = json.load(f) for i in self.plugins.values():
logger.debug("Info: {}".format(info)) self._install_deps(i._info)
sys.path.append(root)
@classmethod
def _install_deps(cls, info=None):
requirements = info.get('requirements', [])
if requirements:
pip_args = []
pip_args.append('install')
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: 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(filename)) logger.debug("No valid plugin for: {}".format(module))
return return
module = candidate(info=info) module = candidate(info=info)
try: repo_path = root
repo_path = root module._repo = Repo(repo_path)
module._repo = Repo(repo_path) except InvalidGitRepositoryError:
except InvalidGitRepositoryError: logger.debug("The plugin {} is not in a Git repository".format(
module._repo = None module))
module._repo = None
except Exception as ex: except Exception as ex:
logger.error("Exception importing {}: {}".format(filename, ex)) logger.error("Exception importing {}: {}".format(module, ex))
logger.error("Trace: {}".format(traceback.format_exc())) logger.error("Trace: {}".format(traceback.format_exc()))
return None, None 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:
@@ -262,8 +297,7 @@ 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("matching {} with {}: {}".format(plug.name, kwargs,
kwargs,
res)) res))
return res return res
@@ -274,5 +308,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

@@ -2,8 +2,8 @@
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,40 @@ 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
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))
@@ -61,15 +67,14 @@ class Context(dict):
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=True, headers=None, **kwargs):
in_headers=False,
headers=None,
**kwargs):
""" """
Return the values and error to be used in flask. Return the values and error to be used in flask.
So far, it returns a fixed context. We should store/generate different So far, it returns a fixed context. We should store/generate different
@@ -86,33 +91,34 @@ class SenpyMixin(object):
'rel="http://www.w3.org/ns/json-ld#context";' 'rel="http://www.w3.org/ns/json-ld#context";'
' type="application/ld+json"' % url) ' type="application/ld+json"' % url)
}) })
return FlaskResponse(json.dumps(js, indent=2, sort_keys=True), return FlaskResponse(
status=getattr(self, "status", 200), json.dumps(
headers=headers, js, indent=2, sort_keys=True),
mimetype="application/json") status=getattr(self, "status", 200),
headers=headers,
mimetype="application/json")
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, with_context=True, context_uri=None): def jsonld(self, with_context=True, context_uri=None):
ser = self.serializable() ser = self.serializable()
if with_context: if with_context:
context = [] context = []
if context_uri: if context_uri:
context = context_uri context = context_uri
@@ -120,9 +126,9 @@ class SenpyMixin(object):
context = self.context.copy() context = self.context.copy()
if hasattr(self, 'prefix'): if hasattr(self, 'prefix'):
# This sets @base for the document, which will be used in # This sets @base for the document, which will be used in
# all relative URIs will. For example, if a uri is "Example" and # all relative URIs. For example, if a uri is "Example" and
# prefix =s "http://example.com", the absolute URI after expanding # prefix =s "http://example.com", the absolute URI after
# with JSON-LD will be "http://example.com/Example" # expanding with JSON-LD will be "http://example.com/Example"
prefix_context = {"@base": self.prefix} prefix_context = {"@base": self.prefix}
if isinstance(context, list): if isinstance(context, list):
@@ -132,10 +138,8 @@ class SenpyMixin(object):
ser["@context"] = context ser["@context"] = context
return ser 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):
@@ -145,34 +149,41 @@ class SenpyMixin(object):
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:
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]
reqs = self.schema.get('required', [])
for i in reqs:
if i not in temp:
prop = self.schema['properties'][i]
if 'default' in prop:
temp[i] = copy.deepcopy(prop['default'])
if 'context' in temp: if 'context' in temp:
context = temp['context'] context = temp['context']
del temp['context'] del temp['context']
self.__dict__['context'] = Context.load(context) self.__dict__['context'] = Context.load(context)
super(SenpyModel, self).__init__(temp) try:
temp['@type'] = getattr(self, '@type')
except AttributeError:
logger.warn('Creating an instance of an unknown model')
super(BaseModel, self).__init__(temp)
def _get_key(self, key): def _get_key(self, key):
key = key.replace("__", ":", 1) key = key.replace("__", ":", 1)
@@ -181,7 +192,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)
@@ -197,52 +207,87 @@ 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_schema(name, schema_file=None, base_classes=None):
schema = read_schema('suggestion.json') base_classes = base_classes or []
base_classes.append(BaseModel)
schema_file = schema_file or '{}.json'.format(name)
class_name = '{}{}'.format(i[0].upper(), i[1:])
newclass = type(class_name, tuple(base_classes), {})
setattr(newclass, '@type', name)
setattr(newclass, 'schema', read_schema(schema_file))
register(newclass, name)
return newclass
class PluginModel(SenpyModel):
schema = read_schema('plugin.json')
class Plugins(SenpyModel): def _add_from_schema(*args, **kwargs):
schema = read_schema('plugins.json') generatedClass = from_schema(*args, **kwargs)
globals()[generatedClass.__name__] = generatedClass
del generatedClass
class Error(SenpyMixin, BaseException ):
def __init__(self, message, status=500, params=None, errors=None, *args, **kwargs): for i in ['response',
'results',
'entry',
'sentiment',
'analysis',
'emotionSet',
'emotion',
'emotionModel',
'suggestion',
'plugin',
'emotionPlugin',
'sentimentPlugin',
'plugins']:
_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 __getattr__(self, key):
return self.__dict__ if key != '_error' and hasattr(self._error, key):
return getattr(self._error, key)
raise AttributeError(key)
def __str__(self): def __setattr__(self, key, value):
return str(self.jsonld()) 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,16 +5,17 @@ import inspect
import os.path import os.path
import pickle import pickle
import logging import logging
from .models import Response, PluginModel, Error import tempfile
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):
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) super(SenpyPlugin, self).__init__(info)
self.id = '{}_{}'.format(self.name, self.version) self.id = '{}_{}'.format(self.name, self.version)
@@ -38,8 +39,8 @@ class SenpyPlugin(PluginModel):
''' Destructor, to make sure all the resources are freed ''' ''' Destructor, to make sure all the resources are freed '''
self.deactivate() self.deactivate()
class SentimentPlugin(SenpyPlugin):
class SentimentPlugin(SenpyPlugin, models.SentimentPlugin):
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))
@@ -47,17 +48,14 @@ class SentimentPlugin(SenpyPlugin):
self["@type"] = "marl:SentimentAnalysis" self["@type"] = "marl:SentimentAnalysis"
class EmotionPlugin(SenpyPlugin): class EmotionPlugin(SentimentPlugin, models.EmotionPlugin):
def __init__(self, info, *args, **kwargs): def __init__(self, info, *args, **kwargs):
resp = super(EmotionPlugin, self).__init__(info, *args, **kwargs)
self.minEmotionValue = float(info.get("minEmotionValue", 0)) self.minEmotionValue = float(info.get("minEmotionValue", 0))
self.maxEmotionValue = float(info.get("maxEmotionValue", 0)) self.maxEmotionValue = float(info.get("maxEmotionValue", 0))
self["@type"] = "onyx:EmotionAnalysis" self["@type"] = "onyx:EmotionAnalysis"
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 +71,18 @@ 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 not hasattr(self, '_shelf_file') or not self._shelf_file:
if hasattr(self, '_info') and 'shelf_file' in self._info: if hasattr(self, '_info') and 'shelf_file' in self._info:
self.__dict__['_shelf_file'] = self._info['shelf_file'] self.__dict__['_shelf_file'] = self._info['shelf_file']
else: else:
self._shelf_file = os.path.join(self.get_folder(), self.name + '.p') self._shelf_file = os.path.join(tempfile.gettempdir(),
self.name + '.p')
return self._shelf_file 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

@@ -1,4 +1,3 @@
import json
import random import random
from senpy.plugins import SentimentPlugin from senpy.plugins import SentimentPlugin
@@ -16,26 +15,15 @@ class Sentiment140Plugin(SentimentPlugin):
polarity = "marl:Positive" polarity = "marl:Positive"
elif polarity_value < 0: elif polarity_value < 0:
polarity = "marl:Negative" polarity = "marl:Negative"
entry = Entry({"id":":Entry0", entry = Entry({"id": ":Entry0", "nif:isString": params["input"]})
"nif:isString": params["input"]}) sentiment = Sentiment({
sentiment = Sentiment({"id": ":Sentiment0", "id": ":Sentiment0",
"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 = []
entry.sentiments.append(sentiment) entry.sentiments.append(sentiment)
entry.language = lang entry.language = lang
response.entries.append(entry) response.entries.append(entry)
return response return response

View File

@@ -9,16 +9,17 @@ class Sentiment140Plugin(SentimentPlugin):
def analyse(self, **params): def analyse(self, **params):
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({
"data": [{"text": params["input"]}] "language": lang,
} "data": [{
) "text": params["input"]
) }]
}))
p = params.get("prefix", None) p = params.get("prefix", None)
response = Results(prefix=p) 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"
neutral_value = self.maxPolarityValue / 2.0 neutral_value = self.maxPolarityValue / 2.0
if polarity_value > neutral_value: if polarity_value > neutral_value:
@@ -26,12 +27,12 @@ class Sentiment140Plugin(SentimentPlugin):
elif polarity_value < neutral_value: elif polarity_value < neutral_value:
polarity = "marl:Negative" polarity = "marl:Negative"
entry = Entry(id="Entry0", entry = Entry(id="Entry0", nif__isString=params["input"])
nif__isString=params["input"]) sentiment = Sentiment(
sentiment = Sentiment(id="Sentiment0", 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 = []
entry.sentiments.append(sentiment) entry.sentiments.append(sentiment)

7
senpy/schemas/\ Normal file
View File

@@ -0,0 +1,7 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Senpy analysis",
"allOf": [{
"$ref": "atom.json"
}]
}

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

@@ -16,16 +16,20 @@
"@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": "prov:wasGeneratedBy"

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,18 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Senpy Emotion analysis",
"type": "object",
"allOf": [
{"$ref": "analysis.json"},
{"properties":
{
"onyx:hasEmotionModel": {
"anyOf": [
{"type": "string"},
{"$ref": "emotionModel.json"}
]
}
},
"required": ["onyx:hasEmotionModel"]
}]
}

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,34 @@
{ {
"$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" }
},
"emotions": {
"type": "array",
"items": {"$ref": "emotionSet.json" }
},
"entities": {
"type": "array",
"items": {"$ref": "entity.json" }
},
"topics": {
"type": "array",
"items": {"$ref": "topic.json" }
},
"suggestions": {
"type": "array",
"items": {"$ref": "suggestion.json" }
}
},
"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": "list",
"items": {"type": "object"}
},
"code": {
"type": "int"
},
"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,4 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "definitions.json#/Suggestion" "type": "object"
} }

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

@@ -60,13 +60,30 @@ body {
border-radius: 20px; border-radius: 20px;
} }
#input_request { #jsonraw {
display:block; overflow: auto;
width:150px; padding: 20px;
word-wrap:break-word; background: lightgray;
-moz-border-radius: 20px;
-webkit-border-radius: 20px;
-khtml-border-radius: 20px;
border-radius: 20px;
} }
textarea #input_request {
{ margin-top: 5px;
display:block;
word-wrap:break-word;
white-space:pre;
overflow: auto;
margin-top: 20px;
margin-bottom: 20px;
}
#input_request a{
width:100%;
}
textarea{
border:1px solid #999999; border:1px solid #999999;
width:100%; width:100%;
margin:5px 0; margin:5px 0;

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

File diff suppressed because one or more lines are too long

View File

@@ -29,6 +29,7 @@ $(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"]){ if (plugins[r]["name"]){
@@ -62,12 +63,17 @@ $(document).ready(function() {
} }
} }
} }
var pluginList = document.createElement('li');
pluginList.innerHTML = "<a href=https://github.com/gsi-upm/senpy-plugins-community>" + plugins[r]["name"] + "</a>" + ": " + plugins[r]["description"]
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();
}); });
@@ -102,8 +108,19 @@ 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 container = document.getElementById('results');
document.getElementById("input_request").innerHTML = "<label>"+url+"</label>" var options = {
mode: 'view'
};
try {
container.removeChild(container.firstChild);
}
catch(err) {
}
var editor = new JSONEditor(container, options, response);
document.getElementById("jsonraw").innerHTML = replaceURLWithHTMLLinks(JSON.stringify(response, undefined, 2))
document.getElementById("input_request").innerHTML = "<a href='"+url+"'>"+url+"</a>"
} }

View File

@@ -10,12 +10,15 @@
<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">
@@ -45,8 +48,6 @@
</div> </div>
</div> </div>
</div>
<div class="col-lg-6 ">
<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-sign-in"></i> Follow us on <a href="http://www.github.com/gsi-upm/senpy">GitHub</a></div>
</div> </div>
@@ -54,6 +55,15 @@
<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 class="col-lg-6 ">
<div class="well">
<h2>Available Plugins</h2>
<div>
<span><ul id=availablePlugins></ul></span>
</div>
</div>
</div>
</div> </div>
</div> </div>
@@ -74,10 +84,25 @@ I cannot believe it!</textarea></div>
<a id="preview" class="btn btn-lg btn-primary" href="#" onclick="load_JSON()">Analyse!</a> <a id="preview" class="btn btn-lg btn-primary" href="#" onclick="load_JSON()">Analyse!</a>
<!--<button id="visualise" name="type" type="button">Visualise!</button>--> <!--<button id="visualise" name="type" type="button">Visualise!</button>-->
</form> </form>
<div id="content">
<span id="input_request"></span> <span id="input_request"></span>
<pre id="results"></pre> <ul class="nav nav-tabs" role="tablist">
</div> <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">
<div class="tab-pane active" id="viewer">
<div id="content">
<pre id="results"></pre>
</div>
</div>
<div class="tab-pane" id="raw">
<div id="content">
<pre id="jsonraw"></pre>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

4
senpy/version.py Normal file
View File

@@ -0,0 +1,4 @@
import os
with open(os.path.join(os.path.dirname(__file__), 'VERSION')) as f:
__version__ = f.read().strip()

View File

@@ -2,3 +2,8 @@
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

View File

@@ -4,8 +4,10 @@ from pip.req import parse_requirements
# parse_requirements() returns generator of pip.req.InstallRequirement objects # parse_requirements() returns generator of pip.req.InstallRequirement objects
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")
@@ -15,30 +17,27 @@ except AttributeError:
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()) from senpy import __version__
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

@@ -3,6 +3,5 @@ from senpy.models import Results
class DummyPlugin(SentimentPlugin): class DummyPlugin(SentimentPlugin):
def analyse(self, *args, **kwargs): def analyse(self, *args, **kwargs):
return Results() return Results()

View File

@@ -4,7 +4,6 @@ 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)

View File

@@ -1,9 +1,10 @@
import os import os
import logging import logging
import json
from senpy.extensions import Senpy from senpy.extensions import Senpy
from flask import Flask from flask import Flask
from flask.ext.testing import TestCase from unittest import TestCase
from gevent import sleep from gevent import sleep
from itertools import product from itertools import product
@@ -12,30 +13,37 @@ 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 json.loads(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
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, 404)
logging.debug(resp.json) js = parse_resp(resp)
assert resp.json["status"] == 404 logging.debug(js)
assert js["status"] == 404
atleast = { atleast = {
"status": 404, "status": 404,
"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 +51,93 @@ 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_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"] == "Dummy_0.1"
def test_activate(self): def test_activate(self):
""" Activate and deactivate one plugin """ """ Activate and deactivate one plugin """
resp = self.client.get("/api/plugins/Dummy/deactivate") resp = self.client.get("/api/plugins/Dummy/deactivate")
self.assert200(resp) self.assertCode(resp, 200)
sleep(0.5) sleep(0.5)
resp = self.client.get("/api/plugins/Dummy/") resp = self.client.get("/api/plugins/Dummy/")
self.assert200(resp) self.assertCode(resp, 200)
assert "is_activated" in resp.json js = parse_resp(resp)
assert resp.json["is_activated"] == False assert "is_activated" in js
assert not js["is_activated"]
resp = self.client.get("/api/plugins/Dummy/activate") resp = self.client.get("/api/plugins/Dummy/activate")
self.assert200(resp) self.assertCode(resp, 200)
sleep(0.5) sleep(0.5)
resp = self.client.get("/api/plugins/Dummy/") resp = self.client.get("/api/plugins/Dummy/")
self.assert200(resp) self.assertCode(resp, 200)
assert "is_activated" in resp.json js = parse_resp(resp)
assert resp.json["is_activated"] == True assert "is_activated" in js
assert js["is_activated"]
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
assert js["@id"] == "Dummy_0.1"
resp = self.client.get("/api/plugins/Dummy/deactivate") resp = self.client.get("/api/plugins/Dummy/deactivate")
self.assert200(resp) self.assertCode(resp, 200)
sleep(0.5) sleep(0.5)
resp = self.client.get("/api/plugins/default/") resp = self.client.get("/api/plugins/default/")
self.assert404(resp) self.assertCode(resp, 404)
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')

42
tests/test_client.py Normal file
View File

@@ -0,0 +1,42 @@
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()
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

@@ -6,18 +6,16 @@ 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, default_plugins=False)
self.senpy.init_app(self.app) self.senpy.init_app(self.app)
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,6 +32,21 @@ 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, '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) == 2
@@ -75,4 +88,5 @@ class ExtensionsTest(TestCase):
assert self.senpy.filter_plugins(name="Dummy", is_activated=True) assert self.senpy.filter_plugins(name="Dummy", is_activated=True)
self.senpy.deactivate_plugin("Dummy", sync=True) self.senpy.deactivate_plugin("Dummy", sync=True)
assert not len( assert not len(
self.senpy.filter_plugins(name="Dummy", is_activated=True)) self.senpy.filter_plugins(
name="Dummy", is_activated=True))

View File

@@ -1,20 +1,16 @@
import os
import logging import logging
import jsonschema import jsonschema
import json import json
import os
from unittest import TestCase from unittest import TestCase
from senpy.models import Response, Entry, Results, Sentiment, EmotionSet, Emotion, Error from senpy.models import Entry, Results, Sentiment, EmotionSet, Error
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", prueba = {"id": "test",
"analysis": [], "analysis": [],
"entries": []} "entries": []}
@@ -27,28 +23,32 @@ class ModelsTest(TestCase):
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 +61,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 +102,16 @@ class ModelsTest(TestCase):
logging.debug(c) logging.debug(c)
p.validate() p.validate()
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_frame_response(self): def test_frame_response(self):
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)
@@ -37,16 +40,28 @@ class ModelsTest(TestCase):
self.shelf_dir = tempfile.mkdtemp() self.shelf_dir = tempfile.mkdtemp()
self.shelf_file = os.path.join(self.shelf_dir, "shelf") self.shelf_file = os.path.join(self.shelf_dir, "shelf")
def test_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,37 @@ 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()
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'

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()