1
0
mirror of https://github.com/gsi-upm/senpy synced 2025-10-19 09:48: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
python:
- "2.7"
- "3.4"
- "3.5"
install: "pip install -r requirements.txt"
# run nosetests - Tests
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 test-requirements.txt
include README.md
include senpy/context.jsonld
include README.rst
include senpy/VERSION
graft senpy/plugins
graft senpy/schemas
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.
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
------------
@@ -30,7 +30,7 @@ Alternatively, you can use the development version:
.. code:: bash
git clone git@github.com:gsi-upm/senpy
git clone http://github.com/gsi-upm/senpy
cd senpy
pip install --user .
@@ -38,9 +38,9 @@ If you want to install senpy globally, use sudo instead of the ``--user`` flag.
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
-----

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 -*-
# flake8: noqa
#
# Senpy documentation build configuration file, created by
# sphinx-quickstart on Tue Feb 24 08:57:32 2015.
@@ -52,16 +53,17 @@ master_doc = 'index'
# General information about the project.
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
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# 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.
release = '0.4'
release = version
# The language for content autogenerated by Sphinx. Refer to documentation
# 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",
"@id": "http://example.com#NIFExample",
"@type": "results",
"analysis": [
],
"entries": [

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
{
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
"@id": "me:Result1",
"@type": "results",
"analysis": [
{
"@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
************
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:
Plugins Interface
=======
- Definition file, has the ".senpy" extension.
- Code file, is a python file.
@@ -34,7 +36,7 @@ The basic methods in a plugin are:
* __init__
* activate: used to load memory-hungry 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.

View File

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

View File

@@ -4,8 +4,9 @@ requests>=2.4.1
GitPython>=0.3.2.RC1
gevent>=1.1rc4
PyLD>=0.6.5
Flask-Testing>=0.4.2
six
future
jsonschema
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
"""
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 gevent.wsgi import WSGIServer
from gevent.monkey import patch_all
import gevent
import logging
import os
import argparse
@@ -34,37 +33,51 @@ patch_all(thread=False)
SERVER_PORT = os.environ.get("PORT", 5000)
def main():
parser = argparse.ArgumentParser(description='Run a Senpy server')
parser.add_argument('--level',
'-l',
metavar='logging_level',
type=str,
default="INFO",
help='Logging level')
parser.add_argument('--debug',
'-d',
action='store_true',
default=False,
help='Run the application in debug mode')
parser.add_argument('--default-plugins',
action='store_true',
default=False,
help='Load the default plugins')
parser.add_argument('--host',
type=str,
default="127.0.0.1",
help='Use 0.0.0.0 to accept requests from any host.')
parser.add_argument('--port',
'-p',
type=int,
default=SERVER_PORT,
help='Port to listen on.')
parser.add_argument('--plugins-folder',
'-f',
type=str,
default='plugins',
help='Where to look for plugins.')
parser.add_argument(
'--level',
'-l',
metavar='logging_level',
type=str,
default="INFO",
help='Logging level')
parser.add_argument(
'--debug',
'-d',
action='store_true',
default=False,
help='Run the application in debug mode')
parser.add_argument(
'--default-plugins',
action='store_true',
default=False,
help='Load the default plugins')
parser.add_argument(
'--host',
type=str,
default="127.0.0.1",
help='Use 0.0.0.0 to accept requests from any host.')
parser.add_argument(
'--port',
'-p',
type=int,
default=SERVER_PORT,
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()
logging.basicConfig()
rl = logging.getLogger()
@@ -72,6 +85,9 @@ def main():
app = Flask(__name__)
app.debug = args.debug
sp = Senpy(app, args.plugins_folder, default_plugins=args.default_plugins)
if args.only_install:
sp.install_deps()
return
sp.activate_all()
http_server = WSGIServer((args.host, args.port), app)
try:
@@ -80,8 +96,10 @@ def main():
args.port))
http_server.serve_forever()
except KeyboardInterrupt:
http_server.stop()
print('Bye!')
http_server.stop()
sp.deactivate_all()
if __name__ == '__main__':
main()

View File

@@ -1,9 +1,8 @@
from future.utils import iteritems
from .models import Error
import logging
logger = logging.getLogger(__name__)
from .models import Error
API_PARAMS = {
"algorithm": {
"aliases": ["algorithm", "a", "algo"],
@@ -25,7 +24,7 @@ CLI_PARAMS = {
"required": True,
"default": "."
},
}
}
NIF_PARAMS = {
"input": {
@@ -96,10 +95,11 @@ def parse_params(indict, spec=NIF_PARAMS):
outdict[param] not in spec[param]["options"]:
wrong_params[param] = spec[param]
if wrong_params:
message = Error(status=404,
message="Missing or invalid parameters",
parameters=outdict,
errors={param: error for param, error in
iteritems(wrong_params)})
message = Error(
status=404,
message="Missing or invalid parameters",
parameters=outdict,
errors={param: error
for param, error in iteritems(wrong_params)})
raise message
return outdict

View File

@@ -17,12 +17,12 @@
"""
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 .api import NIF_PARAMS, WEB_PARAMS, parse_params
from .api import WEB_PARAMS, parse_params
from functools import wraps
import json
import logging
logger = logging.getLogger(__name__)
@@ -30,6 +30,7 @@ logger = logging.getLogger(__name__)
api_blueprint = Blueprint("api", __name__)
demo_blueprint = Blueprint("demo", __name__)
def get_params(req):
if req.method == 'POST':
indict = req.form.to_dict(flat=True)
@@ -44,17 +45,20 @@ def get_params(req):
def index():
return render_template("index.html")
@api_blueprint.route('/contexts/<entity>.jsonld')
def context(entity="context"):
return jsonify({"@context": Response.context})
@api_blueprint.route('/schemas/<schema>')
def schema(schema="definitions"):
try:
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()
def basic_api(f):
@wraps(f)
def decorated_function(*args, **kwargs):
@@ -73,12 +77,15 @@ def basic_api(f):
response = ex
in_headers = web_params["inHeaders"] != "0"
headers = {'X-ORIGINAL-PARAMS': raw_params}
return response.flask(in_headers=in_headers,
headers=headers,
context_uri=url_for('api.context', entity=type(response).__name__,
_external=True))
return response.flask(
in_headers=in_headers,
headers=headers,
context_uri=url_for(
'api.context', entity=type(response).__name__, _external=True))
return decorated_function
@api_blueprint.route('/', methods=['POST', 'GET'])
@basic_api
def api():
@@ -93,13 +100,12 @@ def plugins():
dic = Plugins(plugins=list(sp.plugins.values()))
return dic
@api_blueprint.route('/plugins/<plugin>/', methods=['POST', 'GET'])
@api_blueprint.route('/plugins/<plugin>/<action>', methods=['POST', 'GET'])
@basic_api
def plugin(plugin=None, action="list"):
filt = {}
sp = current_app.senpy
plugs = sp.filter_plugins(name=plugin)
if plugin == 'default' and sp.default_plugin:
response = sp.default_plugin
plugin = response.name
@@ -110,15 +116,8 @@ def plugin(plugin=None, action="list"):
if action == "list":
return response
method = "{}_plugin".format(action)
if(hasattr(sp, method)):
if (hasattr(sp, method)):
getattr(sp, method)(plugin)
return Response(message="Ok")
else:
return Error(message="action '{}' not allowed".format(action))
if __name__ == '__main__':
import config
app.register_blueprint(api_blueprint)
app.debug = config.DEBUG
app.run(host='0.0.0.0', port=5000)

View File

@@ -3,6 +3,7 @@ from .models import Error
from .api import parse_params, CLI_PARAMS
from .extensions import Senpy
def argv_to_dict(argv):
'''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)):
if argv[i][0] == '-':
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] == '-':
cli_dict[key] = ""
else:
cli_dict[key] = value
return cli_dict
def parse_cli(argv):
cli_dict = argv_to_dict(argv)
cli_params = parse_params(cli_dict, spec=CLI_PARAMS)
@@ -34,6 +36,7 @@ def main_function(argv):
res = sp.analyse(**cli_dict)
return res
def main():
'''This method is the entrypoint for the CLI (as configured un setup.py)
'''
@@ -47,4 +50,3 @@ def main():
if __name__ == '__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
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 .blueprints import api_blueprint, demo_blueprint
from .api import API_PARAMS, NIF_PARAMS, parse_params
from git import Repo, InvalidGitRepositoryError
from functools import partial
from threading import Thread
import os
import fnmatch
@@ -21,17 +19,19 @@ import sys
import imp
import logging
import traceback
import gevent
import json
import yaml
import pip
logger = logging.getLogger(__name__)
class Senpy(object):
""" 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._search_folders = set()
@@ -79,22 +79,24 @@ class Senpy(object):
elif self.plugins:
algo = self.default_plugin and self.default_plugin.name
if not algo:
raise Error(status=404,
message=("No plugins found."
" Please install one.").format(algo))
raise Error(
status=404,
message=("No plugins found."
" Please install one.").format(algo))
if algo not in self.plugins:
logger.debug(("The algorithm '{}' is not valid\n"
"Valid algorithms: {}").format(algo,
self.plugins.keys()))
raise Error(status=404,
message="The algorithm '{}' is not valid"
.format(algo))
raise Error(
status=404,
message="The algorithm '{}' is not valid".format(algo))
if not self.plugins[algo].is_activated:
logger.debug("Plugin not activated: {}".format(algo))
raise Error(status=400,
message=("The algorithm '{}'"
" is not activated yet").format(algo))
raise Error(
status=400,
message=("The algorithm '{}'"
" is not activated yet").format(algo))
plug = self.plugins[algo]
nif_params = parse_params(params, spec=NIF_PARAMS)
extra_params = plug.get('extra_params', {})
@@ -119,9 +121,8 @@ class Senpy(object):
return None
def parameters(self, algo):
return getattr(self.plugins.get(algo) or self.default_plugin,
"extra_params",
{})
return getattr(
self.plugins.get(algo) or self.default_plugin, "extra_params", {})
def activate_all(self, sync=False):
ps = []
@@ -145,47 +146,53 @@ class Senpy(object):
try:
plugin = self.plugins[plugin_name]
except KeyError:
raise Error(message="Plugin not found: {}".format(plugin_name),
status=404)
raise Error(
message="Plugin not found: {}".format(plugin_name), status=404)
logger.info("Activating plugin: {}".format(plugin.name))
def act():
success = False
try:
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:
logger.error("Error activating plugin {}: {}".format(plugin.name,
ex))
logger.error("Trace: {}".format(traceback.format_exc()))
th = gevent.spawn(act)
th.link_value(partial(self._set_active_plugin, plugin_name, True))
msg = "Error activating plugin {} - {} : \n\t{}".format(
plugin.name, ex, traceback.format_exc())
logger.error(msg)
raise Error(msg)
if sync:
th.join()
act()
else:
return th
th = Thread(target=act)
th.start()
def deactivate_plugin(self, plugin_name, sync=False):
try:
plugin = self.plugins[plugin_name]
except KeyError:
raise Error(message="Plugin not found: {}".format(plugin_name),
status=404)
raise Error(
message="Plugin not found: {}".format(plugin_name), status=404)
self._set_active_plugin(plugin_name, False)
def deact():
try:
plugin.deactivate()
logger.info("Plugin deactivated: {}".format(plugin.name))
except Exception as ex:
logger.error("Error deactivating plugin {}: {}".format(plugin.name,
ex))
logger.error("Error deactivating plugin {}: {}".format(
plugin.name, ex))
logger.error("Trace: {}".format(traceback.format_exc()))
th = gevent.spawn(deact)
th.link_value(partial(self._set_active_plugin, plugin_name, False))
if sync:
th.join()
deact()
else:
return th
th = Thread(target=deact)
th.start()
def reload_plugin(self, name):
logger.debug("Reloading {}".format(name))
@@ -198,43 +205,71 @@ class Senpy(object):
logger.error('Error reloading {}: {}'.format(name, ex))
self.plugins[name] = plugin
@staticmethod
def _load_plugin(root, filename):
logger.debug("Loading plugin: {}".format(filename))
fpath = os.path.join(root, filename)
with open(fpath, 'r') as f:
info = json.load(f)
logger.debug("Info: {}".format(info))
sys.path.append(root)
@classmethod
def validate_info(cls, info):
return all(x in info for x in ('name', 'module', 'version'))
def install_deps(self):
for i in self.plugins.values():
self._install_deps(i._info)
@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"]
name = info["name"]
sys.path.append(root)
(fp, pathname, desc) = imp.find_module(module, [root, ])
try:
cls._install_deps(info)
tmp = imp.load_module(module, fp, pathname, desc)
sys.path.remove(root)
candidate = None
for _, obj in inspect.getmembers(tmp):
if inspect.isclass(obj) and inspect.getmodule(obj) == tmp:
logger.debug(("Found plugin class:"
" {}@{}").format(obj, inspect.getmodule(obj))
)
" {}@{}").format(obj, inspect.getmodule(
obj)))
candidate = obj
break
if not candidate:
logger.debug("No valid plugin for: {}".format(filename))
logger.debug("No valid plugin for: {}".format(module))
return
module = candidate(info=info)
try:
repo_path = root
module._repo = Repo(repo_path)
except InvalidGitRepositoryError:
module._repo = None
repo_path = root
module._repo = Repo(repo_path)
except InvalidGitRepositoryError:
logger.debug("The plugin {} is not in a Git repository".format(
module))
module._repo = None
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()))
return None, None
return name, module
@classmethod
def _load_plugin(cls, root, filename):
fpath = os.path.join(root, filename)
logger.debug("Loading plugin: {}".format(fpath))
with open(fpath, 'r') as f:
info = yaml.load(f)
logger.debug("Info: {}".format(info))
return cls._load_plugin_from_info(info, root)
def _load_plugins(self):
plugins = {}
for search_folder in self._search_folders:
@@ -262,8 +297,7 @@ class Senpy(object):
def matches(plug):
res = all(getattr(plug, k, None) == v for (k, v) in kwargs.items())
logger.debug("matching {} with {}: {}".format(plug.name,
kwargs,
logger.debug("matching {} with {}: {}".format(plug.name, kwargs,
res))
return res
@@ -274,5 +308,8 @@ class Senpy(object):
def sentiment_plugins(self):
""" Return only the sentiment plugins """
return {p: plugin for p, plugin in self.plugins.items() if
isinstance(plugin, SentimentPlugin)}
return {
p: plugin
for p, plugin in self.plugins.items()
if isinstance(plugin, SentimentPlugin)
}

View File

@@ -2,8 +2,8 @@
Senpy Models.
This implementation should mirror the JSON schema definition.
For compatibility with Py3 and for easier debugging, this new version drops introspection
and adds all arguments to the models.
For compatibility with Py3 and for easier debugging, this new version drops
introspection and adds all arguments to the models.
'''
from __future__ import print_function
from six import string_types
@@ -12,34 +12,40 @@ import time
import copy
import json
import os
import logging
import jsonref
import jsonschema
from flask import Response as FlaskResponse
import logging
logger = logging.getLogger(__name__)
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):
if absolute:
return os.path.realpath(schema_file)
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):
schema_path = get_schema_path(schema_file, absolute)
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)
logging.debug(base_schema)
class Context(dict):
@staticmethod
def load(context):
logging.debug('Loading context: {}'.format(context))
@@ -61,15 +67,14 @@ class Context(dict):
else:
raise AttributeError('Please, provide a valid context')
base_context = Context.load(CONTEXT_PATH)
class SenpyMixin(object):
context = base_context["@context"]
def flask(self,
in_headers=False,
headers=None,
**kwargs):
def flask(self, in_headers=True, headers=None, **kwargs):
"""
Return the values and error to be used in flask.
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";'
' type="application/ld+json"' % url)
})
return FlaskResponse(json.dumps(js, indent=2, sort_keys=True),
status=getattr(self, "status", 200),
headers=headers,
mimetype="application/json")
return FlaskResponse(
json.dumps(
js, indent=2, sort_keys=True),
status=getattr(self, "status", 200),
headers=headers,
mimetype="application/json")
def serializable(self):
def ser_or_down(item):
if hasattr(item, 'serializable'):
return item.serializable()
elif isinstance(item, dict):
temp = dict()
for kp in item:
vp = item[kp]
temp[kp] = ser_or_down(vp)
return temp
elif isinstance(item, list):
return list(ser_or_down(i) for i in item)
else:
return item
return ser_or_down(self._plain_dict())
if hasattr(item, 'serializable'):
return item.serializable()
elif isinstance(item, dict):
temp = dict()
for kp in item:
vp = item[kp]
temp[kp] = ser_or_down(vp)
return temp
elif isinstance(item, list):
return list(ser_or_down(i) for i in item)
else:
return item
return ser_or_down(self._plain_dict())
def jsonld(self, with_context=True, context_uri=None):
ser = self.serializable()
if with_context:
if with_context:
context = []
if context_uri:
context = context_uri
@@ -120,9 +126,9 @@ class SenpyMixin(object):
context = self.context.copy()
if hasattr(self, 'prefix'):
# This sets @base for the document, which will be used in
# all relative URIs will. For example, if a uri is "Example" and
# prefix =s "http://example.com", the absolute URI after expanding
# with JSON-LD will be "http://example.com/Example"
# all relative URIs. For example, if a uri is "Example" and
# prefix =s "http://example.com", the absolute URI after
# expanding with JSON-LD will be "http://example.com/Example"
prefix_context = {"@base": self.prefix}
if isinstance(context, list):
@@ -132,10 +138,8 @@ class SenpyMixin(object):
ser["@context"] = context
return ser
def to_JSON(self, *args, **kwargs):
js = json.dumps(self.jsonld(*args, **kwargs), indent=4,
sort_keys=True)
js = json.dumps(self.jsonld(*args, **kwargs), indent=4, sort_keys=True)
return js
def validate(self, obj=None):
@@ -145,34 +149,41 @@ class SenpyMixin(object):
obj = obj.jsonld()
jsonschema.validate(obj, self.schema)
class SenpyModel(SenpyMixin, dict):
def __str__(self):
return str(self.to_JSON())
class BaseModel(SenpyMixin, dict):
schema = base_schema
def __init__(self, *args, **kwargs):
self.id = kwargs.pop('id', '{}_{}'.format(type(self).__name__,
time.time()))
if 'id' in kwargs:
self.id = kwargs.pop('id')
elif kwargs.pop('_auto_id', True):
self.id = '_:{}_{}'.format(
type(self).__name__, time.time())
temp = dict(*args, **kwargs)
for obj in [self.schema, ] + self.schema.get('allOf', []):
for k, v in obj.get('properties', {}).items():
if 'default' in v:
temp[k] = copy.deepcopy(v['default'])
for i in temp:
nk = self._get_key(i)
if nk != i:
temp[nk] = temp[i]
del temp[i]
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:
context = temp['context']
del temp['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):
key = key.replace("__", ":", 1)
@@ -181,7 +192,6 @@ class SenpyModel(SenpyMixin, dict):
def __setitem__(self, key, value):
dict.__setitem__(self, key, value)
def __delitem__(self, key):
dict.__delitem__(self, key)
@@ -197,52 +207,87 @@ class SenpyModel(SenpyMixin, dict):
def __delattr__(self, key):
self.__delitem__(self._get_key(key))
def _plain_dict(self):
d = { k: v for (k,v) in self.items() if k[0] != "_"}
d["@id"] = d.pop('id')
d = {k: v for (k, v) in self.items() if k[0] != "_"}
if 'id' in d:
d["@id"] = d.pop('id')
return d
class Response(SenpyModel):
schema = read_schema('response.json')
class Results(SenpyModel):
schema = read_schema('results.json')
_subtypes = {}
class Entry(SenpyModel):
schema = read_schema('entry.json')
class Sentiment(SenpyModel):
schema = read_schema('sentiment.json')
def register(rsubclass, rtype=None):
_subtypes[rtype or rsubclass.__name__] = rsubclass
class Analysis(SenpyModel):
schema = read_schema('analysis.json')
class EmotionSet(SenpyModel):
schema = read_schema('emotionSet.json')
def from_dict(indict):
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):
schema = read_schema('suggestion.json')
def from_schema(name, schema_file=None, base_classes=None):
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):
schema = read_schema('plugins.json')
def _add_from_schema(*args, **kwargs):
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.status = status
self.params = params or {}
self.errors = errors or ""
def _plain_dict(self):
return self.__dict__
def __getattr__(self, key):
if key != '_error' and hasattr(self._error, key):
return getattr(self._error, key)
raise AttributeError(key)
def __str__(self):
return str(self.jsonld())
def __setattr__(self, key, value):
if key != '_error':
return setattr(self._error, key, value)
else:
super(Error, self).__setattr__(key, value)
def __delattr__(self, key):
delattr(self._error, key)
register(Error, 'error')

View File

@@ -5,16 +5,17 @@ import inspect
import os.path
import pickle
import logging
from .models import Response, PluginModel, Error
import tempfile
from . import models
logger = logging.getLogger(__name__)
class SenpyPlugin(PluginModel):
class SenpyPlugin(models.Plugin):
def __init__(self, info=None):
if not info:
raise Error(message=("You need to provide configuration"
"information for the plugin."))
raise models.Error(message=("You need to provide configuration"
"information for the plugin."))
logger.debug("Initialising {}".format(info))
super(SenpyPlugin, self).__init__(info)
self.id = '{}_{}'.format(self.name, self.version)
@@ -38,8 +39,8 @@ class SenpyPlugin(PluginModel):
''' Destructor, to make sure all the resources are freed '''
self.deactivate()
class SentimentPlugin(SenpyPlugin):
class SentimentPlugin(SenpyPlugin, models.SentimentPlugin):
def __init__(self, info, *args, **kwargs):
super(SentimentPlugin, self).__init__(info, *args, **kwargs)
self.minPolarityValue = float(info.get("minPolarityValue", 0))
@@ -47,17 +48,14 @@ class SentimentPlugin(SenpyPlugin):
self["@type"] = "marl:SentimentAnalysis"
class EmotionPlugin(SenpyPlugin):
class EmotionPlugin(SentimentPlugin, models.EmotionPlugin):
def __init__(self, info, *args, **kwargs):
resp = super(EmotionPlugin, self).__init__(info, *args, **kwargs)
self.minEmotionValue = float(info.get("minEmotionValue", 0))
self.maxEmotionValue = float(info.get("maxEmotionValue", 0))
self["@type"] = "onyx:EmotionAnalysis"
class ShelfMixin(object):
@property
def sh(self):
if not hasattr(self, '_sh') or self._sh is None:
@@ -73,22 +71,18 @@ class ShelfMixin(object):
del self.__dict__['_sh']
self.save()
def __del__(self):
self.save()
super(ShelfMixin, self).__del__()
@property
def shelf_file(self):
if not hasattr(self, '_shelf_file') or not self._shelf_file:
if hasattr(self, '_info') and 'shelf_file' in self._info:
self.__dict__['_shelf_file'] = self._info['shelf_file']
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
def save(self):
logger.debug('closing pickle')
logger.debug('saving pickle')
if hasattr(self, '_sh') and self._sh is not None:
with open(self.shelf_file, 'wb') as f:
pickle.dump(self._sh, f)
del(self.__dict__['_sh'])

View File

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

View File

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

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#",
"$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"
},
"suggestions": {
"@id": "me:hasSuggestions"
"@id": "me:hasSuggestions",
"@container": "@set"
},
"emotions": {
"@id": "onyx:hasEmotionSet"
"@id": "onyx:hasEmotionSet",
"@container": "@set"
},
"sentiments": {
"@id": "marl:hasOpinion"
"@id": "marl:hasOpinion",
"@container": "@set"
},
"entries": {
"@id": "prov:used"
"@id": "prov:used",
"@container": "@set"
},
"analysis": {
"@id": "prov:wasGeneratedBy"

View File

@@ -1,169 +1,45 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"Results": {
"title": "Results",
"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"]
"$ref": "results.json"
},
"Context": {
"description": "JSON-LD Context",
"type": ["array", "string", "object"]
"$ref": "context.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"]
"$ref": "analysis.json"
},
"Entry": {
"properties": {
"@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"]
"$ref": "entry.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"]
"$ref": "sentiment.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"
},
"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"]
"$ref": "emotionSet.json"
},
"Emotion": {
"type": "object"
"$ref": "emotion.json"
},
"EmotionModel": {
"$ref": "emotionModel.json"
},
"Entity": {
"type": "object"
"$ref": "entity.json"
},
"Topic": {
"type": "object"
"$ref": "topic.json"
},
"Suggestion": {
"type": "object"
"$ref": "suggestion.json"
},
"Plugins": {
"properties": {
"plugins": {
"type": "array",
"items": {
"$ref": "#/Plugin"
}
}
}
"$ref": "plugin.json"
},
"Plugin": {
"type": "object",
"required": ["@id", "extra_params"],
"properties": {
"@id": {
"type": "string"
},
"extra_params": {
"type": "object",
"default": {}
}
}
"$ref": "plugin.json"
},
"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#",
"$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#",
"$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#",
"$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#",
"$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#",
"$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#",
"$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#",
"$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;
}
#input_request {
display:block;
width:150px;
word-wrap:break-word;
#jsonraw {
overflow: auto;
padding: 20px;
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;
width:100%;
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 defaultPlugin= JSON.parse($.ajax({type: "GET", url: "/api/plugins/default" , async: false}).responseText);
html="";
var availablePlugins = document.getElementById('availablePlugins');
plugins = response.plugins;
for (r in plugins){
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;
change_params();
$(window).on('hashchange', hashchanged);
hashchanged();
$('.tooltip-form').tooltip();
});
@@ -102,8 +108,19 @@ function load_JSON(){
}
}
var response = JSON.parse($.ajax({type: "GET", url: url , async: false}).responseText);
document.getElementById("results").innerHTML = replaceURLWithHTMLLinks(JSON.stringify(response, undefined, 2))
document.getElementById("input_request").innerHTML = "<label>"+url+"</label>"
var container = document.getElementById('results');
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/main.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/jsoneditor.js"></script>
<script src="static/js/main.js"></script>
<body>
<div class="container">
<div id="header">
@@ -45,8 +48,6 @@
</div>
</div>
</div>
<div class="col-lg-6 ">
<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>
@@ -54,6 +55,15 @@
<div class="panel-heading"><i class="fa fa-child"></i> Enjoy.</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>
@@ -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>
<!--<button id="visualise" name="type" type="button">Visualise!</button>-->
</form>
<div id="content">
<span id="input_request"></span>
<pre id="results"></pre>
</div>
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a class="active" href="#viewer">Viewer</a></li>
<li role="presentation"><a class="active" href="#raw">Raw</a></li>
</ul>
<div class="tab-content">
<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>

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
[aliases]
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
try:
install_reqs = parse_requirements("requirements.txt", session=pip.download.PipSession())
test_reqs = parse_requirements("test-requirements.txt", session=pip.download.PipSession())
install_reqs = parse_requirements(
"requirements.txt", session=pip.download.PipSession())
test_reqs = parse_requirements(
"test-requirements.txt", session=pip.download.PipSession())
except AttributeError:
install_reqs = parse_requirements("requirements.txt")
test_reqs = parse_requirements("test-requirements.txt")
@@ -15,30 +17,27 @@ except AttributeError:
install_reqs = [str(ir.req) for ir in install_reqs]
test_reqs = [str(ir.req) for ir in test_reqs]
exec(open('senpy/__init__.py').read())
from senpy import __version__
setup(
name='senpy',
packages=['senpy'], # this must be the same as the name above
version=__version__,
description='''
A sentiment analysis server implementation. Designed to be \
extendable, so new algorithms and sources can be used.
''',
description=('A sentiment analysis server implementation. '
'Designed to be extensible, so new algorithms '
'and sources can be used.'),
author='J. Fernando Sanchez',
author_email='balkian@gmail.com',
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'],
classifiers=[],
install_requires=install_reqs,
tests_require=test_reqs,
setup_requires=['pytest-runner',],
setup_requires=['pytest-runner', ],
include_package_data=True,
entry_points={
'console_scripts': [
'senpy = senpy.__main__:main',
'senpy-cli = senpy.cli:main'
]
}
)
'console_scripts':
['senpy = senpy.__main__:main', 'senpy-cli = senpy.cli:main']
})

View File

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

View File

@@ -4,7 +4,6 @@ from time import sleep
class SleepPlugin(SenpyPlugin):
def activate(self, *args, **kwargs):
sleep(self.timeout)

View File

@@ -1,9 +1,10 @@
import os
import logging
import json
from senpy.extensions import Senpy
from flask import Flask
from flask.ext.testing import TestCase
from unittest import TestCase
from gevent import sleep
from itertools import product
@@ -12,30 +13,37 @@ def check_dict(indic, template):
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.client = self.app.test_client()
self.senpy = Senpy()
self.senpy.init_app(self.app)
self.dir = os.path.join(os.path.dirname(__file__), "..")
self.senpy.add_folder(self.dir)
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):
"""
Calling with no arguments should ask the user for more arguments
"""
resp = self.client.get("/api/")
self.assert404(resp)
logging.debug(resp.json)
assert resp.json["status"] == 404
self.assertCode(resp, 404)
js = parse_resp(resp)
logging.debug(js)
assert js["status"] == 404
atleast = {
"status": 404,
"message": "Missing or invalid parameters",
}
assert check_dict(resp.json, atleast)
assert check_dict(js, atleast)
def test_analysis(self):
"""
@@ -43,81 +51,93 @@ class BlueprintsTest(TestCase):
it should contain the context
"""
resp = self.client.get("/api/?i=My aloha mohame")
self.assert200(resp)
logging.debug("Got response: %s", resp.json)
assert "@context" in resp.json
assert "entries" in resp.json
self.assertCode(resp, 200)
js = parse_resp(resp)
logging.debug("Got response: %s", js)
assert "@context" in js
assert "entries" in js
def test_list(self):
""" List the plugins """
resp = self.client.get("/api/plugins/")
self.assert200(resp)
logging.debug(resp.json)
assert 'plugins' in resp.json
plugins = resp.json['plugins']
self.assertCode(resp, 200)
js = parse_resp(resp)
logging.debug(js)
assert 'plugins' in js
plugins = js['plugins']
assert len(plugins) > 1
assert list(p for p in plugins if p['name'] == "Dummy")
assert "@context" in resp.json
assert "@context" in js
def test_headers(self):
for i, j in product(["/api/plugins/?nothing=", "/api/?i=test&"],
["inHeaders"]):
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))
assert "@context" in resp.json
js = parse_resp(resp)
assert "@context" in js
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))
assert "@context" not in resp.json
js = parse_resp(resp)
assert "@context" not in js
def test_detail(self):
""" Show only one plugin"""
resp = self.client.get("/api/plugins/Dummy/")
self.assert200(resp)
logging.debug(resp.json)
assert "@id" in resp.json
assert resp.json["@id"] == "Dummy_0.1"
self.assertCode(resp, 200)
js = parse_resp(resp)
logging.debug(js)
assert "@id" in js
assert js["@id"] == "Dummy_0.1"
def test_activate(self):
""" Activate and deactivate one plugin """
resp = self.client.get("/api/plugins/Dummy/deactivate")
self.assert200(resp)
self.assertCode(resp, 200)
sleep(0.5)
resp = self.client.get("/api/plugins/Dummy/")
self.assert200(resp)
assert "is_activated" in resp.json
assert resp.json["is_activated"] == False
self.assertCode(resp, 200)
js = parse_resp(resp)
assert "is_activated" in js
assert not js["is_activated"]
resp = self.client.get("/api/plugins/Dummy/activate")
self.assert200(resp)
self.assertCode(resp, 200)
sleep(0.5)
resp = self.client.get("/api/plugins/Dummy/")
self.assert200(resp)
assert "is_activated" in resp.json
assert resp.json["is_activated"] == True
self.assertCode(resp, 200)
js = parse_resp(resp)
assert "is_activated" in js
assert js["is_activated"]
def test_default(self):
""" Show only one plugin"""
resp = self.client.get("/api/plugins/default/")
self.assert200(resp)
logging.debug(resp.json)
assert "@id" in resp.json
assert resp.json["@id"] == "Dummy_0.1"
self.assertCode(resp, 200)
js = parse_resp(resp)
logging.debug(js)
assert "@id" in js
assert js["@id"] == "Dummy_0.1"
resp = self.client.get("/api/plugins/Dummy/deactivate")
self.assert200(resp)
self.assertCode(resp, 200)
sleep(0.5)
resp = self.client.get("/api/plugins/default/")
self.assert404(resp)
self.assertCode(resp, 404)
def test_context(self):
resp = self.client.get("/api/contexts/context.jsonld")
self.assert200(resp)
assert "@context" in resp.json
self.assertCode(resp, 200)
js = parse_resp(resp)
assert "@context" in js
assert check_dict(
resp.json["@context"],
js["@context"],
{"marl": "http://www.gsi.dit.upm.es/ontologies/marl/ns#"})
def test_schema(self):
resp = self.client.get("/api/schemas/definitions.json")
self.assert200(resp)
assert "$schema" in resp.json
self.assertCode(resp, 200)
js = parse_resp(resp)
assert "$schema" in js

View File

@@ -1,7 +1,11 @@
import os
import logging
from functools import partial
try:
from unittest.mock import patch
except ImportError:
from mock import patch
logger = logging.getLogger(__name__)
from unittest import TestCase
@@ -10,12 +14,14 @@ from senpy.models import Error
class CLITest(TestCase):
def test_basic(self):
self.assertRaises(Error, partial(main_function, []))
res = main_function(['--input', 'test'])
assert 'entries' in res
res = main_function(['--input', 'test', '--algo', 'rand'])
assert 'entries' in res
assert 'analysis' in res
assert res['analysis'][0]['name'] == 'rand'
with patch('senpy.extensions.Senpy.analyse') as patched:
main_function(['--input', 'test'])
patched.assert_called_with(input='test')
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.models import Error
from flask import Flask
from flask.ext.testing import TestCase
from unittest import TestCase
class ExtensionsTest(TestCase):
def create_app(self):
def setUp(self):
self.app = Flask("test_extensions")
self.dir = os.path.join(os.path.dirname(__file__))
self.senpy = Senpy(plugin_folder=self.dir, default_plugins=False)
self.senpy.init_app(self.app)
self.senpy.activate_plugin("Dummy", sync=True)
return self.app
def test_init(self):
""" Initialising the app with the extension. """
@@ -34,6 +32,21 @@ class ExtensionsTest(TestCase):
assert "Dummy" in self.senpy.plugins
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 """
self.senpy.activate_all(sync=True)
assert len(self.senpy.plugins) == 2
@@ -75,4 +88,5 @@ class ExtensionsTest(TestCase):
assert self.senpy.filter_plugins(name="Dummy", is_activated=True)
self.senpy.deactivate_plugin("Dummy", sync=True)
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 jsonschema
import json
import os
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 pprint import pprint
class ModelsTest(TestCase):
def test_jsonld(self):
ctx = os.path.normpath(os.path.join(__file__, "..", "..", "..", "senpy", "schemas", "context.jsonld"))
prueba = {"id": "test",
"analysis": [],
"entries": []}
@@ -27,28 +23,32 @@ class ModelsTest(TestCase):
j = r.jsonld(with_context=True)
print("As JSON:")
pprint(j)
assert("@context" in j)
assert("marl" in j["@context"])
assert("entries" in j["@context"])
assert(j["@id"] == "test")
assert ("@context" in j)
assert ("marl" in j["@context"])
assert ("entries" in j["@context"])
assert (j["@id"] == "test")
assert "id" not in j
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)
assert("marl" in r6.context)
assert("entries" in r6.context)
assert ("marl" in r6.context)
assert ("entries" in r6.context)
j6 = r6.jsonld(with_context=True)
logging.debug("jsonld: %s", j6)
assert("@context" in j6)
assert("entries" in j6)
assert("analysis" in j6)
assert ("@context" in j6)
assert ("entries" in j6)
assert ("analysis" in j6)
resp = r6.flask()
received = json.loads(resp.data.decode())
logging.debug("Response: %s", j6)
assert(received["entries"])
assert(received["entries"][0]["nif:isString"] == "Just testing")
assert(received["entries"][0]["nif:isString"] != "Not testing")
assert (received["entries"])
assert (received["entries"][0]["nif:isString"] == "Just testing")
assert (received["entries"][0]["nif:isString"] != "Not testing")
def test_id(self):
''' Adding the id after creation should overwrite the automatic ID
@@ -61,7 +61,6 @@ class ModelsTest(TestCase):
assert j2['@id'] == 'test'
assert 'id' not in j2
def test_entries(self):
e = Entry()
self.assertRaises(jsonschema.ValidationError, e.validate)
@@ -103,5 +102,16 @@ class ModelsTest(TestCase):
logging.debug(c)
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):
pass

View File

@@ -1,31 +1,34 @@
#!/bin/env python
import os
import logging
import pickle
import shutil
import tempfile
import json
import os
from unittest import TestCase
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):
assert key in self.sh
print('Checking: sh[{}] == {}'.format(key, value))
print('SH[{}]: {}'.format(key, self.sh[key]))
assert self.sh[key] == value
class ModelsTest(TestCase):
def deactivate(self, *args, **kwargs):
self.save()
def analyse(self, *args, **kwargs):
self.sh['counter'] = self.sh['counter'] + 1
e = Entry()
e.nif__isString = self.sh['counter']
r = Results()
r.entries.append(e)
return r
class PluginsTest(TestCase):
def tearDown(self):
if os.path.exists(self.shelf_dir):
shutil.rmtree(self.shelf_dir)
@@ -37,16 +40,28 @@ class ModelsTest(TestCase):
self.shelf_dir = tempfile.mkdtemp()
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):
''' A shelf is created and the value is stored '''
a = ShelfTest(info={'name': 'shelve',
'version': 'test',
'shelf_file': self.shelf_file})
a = ShelfDummyPlugin(info={
'name': 'shelve',
'version': 'test',
'shelf_file': self.shelf_file
})
assert a.sh == {}
a.activate()
assert a.sh == {'counter': 0}
assert a.shelf_file == self.shelf_file
a.sh['a'] = 'fromA'
a.test(key='a', value='fromA')
assert a.sh['a'] == 'fromA'
a.save()
@@ -54,19 +69,37 @@ class ModelsTest(TestCase):
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):
''' Reusing the values of a previous shelf '''
a = ShelfTest(info={'name': 'shelve',
'version': 'test',
'shelf_file': self.shelf_file})
a = ShelfDummyPlugin(info={
'name': 'shelve',
'version': 'test',
'shelf_file': self.shelf_file
})
a.activate()
print('Shelf file: %s' % a.shelf_file)
a.sh['a'] = 'fromA'
a.save()
b = ShelfTest(info={'name': 'shelve',
'version': 'test',
'shelf_file': self.shelf_file})
b.test(key='a', value='fromA')
b = ShelfDummyPlugin(info={
'name': 'shelve',
'version': 'test',
'shelf_file': self.shelf_file
})
b.activate()
assert b.sh['a'] == 'fromA'
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()