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

Compare commits

..

138 Commits

Author SHA1 Message Date
J. Fernando Sánchez
5493070d40 Filter conversion plugins
Closes #12

* Shows only analysis plugins by default on /api/plugins
* Adds a plugin_type parameter to get other types of plugins
* default_plugin chosen from analysis plugins
2017-03-06 11:27:49 +01:00
J. Fernando Sánchez
cbeb3adbdb Added fallback version '0.0'
Installing depends on the VERSION file, so it raies an error if it is
installed in some other way.

ReadTheDocs installs the package so it can generate code docs.
This commit adds a default version 0.0
2017-03-01 18:53:54 +01:00
J. Fernando Sánchez
efb305173e Removed future from __init__
Since __init__ is imported by setup.py, future may not be installed yet.

Other options would be:

* Read VERSION -> and that code has to be duplicated in setup.py and
  senpy (to avoid the import, once again)
* Eval version.py
* Do without versioning :)
2017-03-01 18:28:20 +01:00
J. Fernando Sánchez
2288b04c92 Remove iteritems for py2/3 compatibility 2017-03-01 18:14:44 +01:00
J. Fernando Sánchez
7899cb4d33 Fixed docker upload
Doing docker push without a tag makes the client upload **ALL** the
images it has for that repo.
2017-03-01 17:59:35 +01:00
J. Fernando Sánchez
62ddca79ac Fixed conversion docs 2017-03-01 17:56:17 +01:00
J. Fernando Sánchez
99403b3443 Fix for async
Should fix #11
2017-03-01 12:25:07 +01:00
J. Fernando Sánchez
a0ff528a4b Improved docs and client
* Client now raises an exception on error
* Added conversion to the documentation
2017-02-28 19:38:01 +01:00
J. Fernando Sánchez
97bd245dfc Changed data directory 2017-02-28 18:31:43 +01:00
J. Fernando Sánchez
d8b59d06a4 Converted Ekman2VAD to centroids
* Changed the way modules are imported -> we can now use dotted
  notation (e.g. senpy.plugins.conversion.centroids)
* Refactored ekman2vad's plugin -> generic centroids
* Added some basic tests
2017-02-28 05:28:55 +01:00
J. Fernando Sánchez
453b9f3257 Fixed bugs in Ekman2VAD 2017-02-28 04:01:05 +01:00
J. Fernando Sánchez
5fb858f5fc Fixed error when installing dependencies 2017-02-28 02:24:49 +01:00
J. Fernando Sánchez
bd984a1437 Fix 5 2017-02-27 21:22:10 +01:00
J. Fernando Sánchez
e741b565a1 Fix 4 2017-02-27 20:44:27 +01:00
J. Fernando Sánchez
668a803d89 Will anything break this time? We shall see 2017-02-27 20:38:55 +01:00
J. Fernando Sánchez
9daae8dda7 Please, please, please let it pass!
Am I a complete moron?
2017-02-27 20:22:55 +01:00
J. Fernando Sánchez
c72094b94b Fixed IMAGE names in GL CI 2017-02-27 20:08:10 +01:00
J. Fernando Sánchez
15d456d048 Testing docker in travis 2017-02-27 19:51:53 +01:00
J. Fernando Sánchez
fef06d4333 Fixed image creation issue with GL CI 2017-02-27 19:37:53 +01:00
J. Fernando Sánchez
454aa61fba Fixed CI problem 2017-02-27 19:31:52 +01:00
J. Fernando Sánchez
ba2e18125c Deployment changes
* Docker all the things!
* Make all the things!
* Fixed version.sh
2017-02-27 19:16:43 +01:00
J. Fernando Sánchez
9f6a6f5ecd Loads of changes!
* Added conversion plugins (API might change!)
* Added conversion to the analysis pipeline
* Changed behaviour of --default-plugins (it adds conversion plugins regardless)
* Added emotionModel [sic] and emotionConversion models

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

Break compatibilitity:

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

Closes #8

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

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

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

* added drone-ci integration: tests for py2.7 and py3
2016-02-20 18:19:52 +01:00
J. Fernando Sánchez
14c9f61864 Python 3 compatible
There are also some slight changes to the JSON schemas and the use of
JSON-LD.
2016-02-20 16:17:37 +01:00
J. Fernando Sánchez
a79df7a3da Closer to py3 2016-02-20 16:07:47 +01:00
J. Fernando Sánchez
ecc2a8312a Still not functional 2016-02-20 16:07:47 +01:00
J. Fernando Sánchez
aafd6a0938 Merge branch 'nacho' 2016-02-19 20:00:21 +01:00
NachoCP
b88d6c53e0 Update Dockerfile 2016-02-10 11:25:20 +01:00
NachoCP
4f2aee5681 DockerFile updated to deploy plugins 2016-02-03 13:43:31 +01:00
NachoCP
a5c27bcaba Test Changed 2016-02-02 11:34:30 +01:00
NachoCP
cefd6331e0 Test Changed 2016-02-02 11:31:23 +01:00
NachoCP
c2bb93e86c Test Changed 2016-02-02 11:27:56 +01:00
NachoCP
091104bc7d Test Changed 2016-02-02 11:13:17 +01:00
NachoCP
81cbe5ea54 Test Changed 2016-02-02 11:09:56 +01:00
NachoCP
ab2c1f73e4 Test Changed 2016-02-02 11:03:47 +01:00
NachoCP
6a84af1c5a Test Changed 2016-02-02 10:44:13 +01:00
NachoCP
5983493b78 Test Changed 2016-02-02 10:39:59 +01:00
NachoCP
61deabe13e DockerFile 2015-12-22 16:04:39 +01:00
J. Fernando Sánchez
bb1b4d3266 Added analyse to the docs 2015-12-17 19:48:28 +01:00
NachoCP
703fb68b27 Merge branch 'master' of github.com:gsi-upm/senpy into nacho 2015-12-14 09:27:31 +01:00
J. Fernando Sánchez
6fe68e3c40 Fixes #3 2015-12-11 14:53:30 +01:00
NachoCP
7b9f8a8bef Merge branch 'master' of github.com:gsi-upm/senpy into nacho 2015-12-11 09:49:35 +01:00
J. Fernando Sánchez
82496dc8e4 Trying to fix an issue with ShelfPlugin 2015-12-10 16:13:22 +01:00
J. Fernando Sánchez
f74ee668b6 Cleaning 2015-11-27 15:05:59 +01:00
NachoCP
d304dec2f7 Update Playground 2015-11-17 11:42:16 +01:00
J. Fernando Sánchez
45838e7e98 Wording :) 2015-11-05 19:20:13 +01:00
J. Fernando Sánchez
ff002c818a Bumped to 0.4.10 2015-11-05 19:18:38 +01:00
J. Fernando Sánchez
79d6b6f67f Added plugins FAQ to the docs 2015-11-05 19:11:35 +01:00
J. Fernando Sánchez
b8993f7d64 Added shelve mixin 2015-11-05 18:50:37 +01:00
J. Fernando Sánchez
bd2e0f0d5c Added traceback to plugin activation 2015-11-05 18:48:07 +01:00
J. Fernando Sánchez
7de5b41340 Started readthedocs and improved README 2015-10-28 21:25:56 +01:00
J. Fernando Sánchez
a63e9209fd Added gitignore 2015-10-28 15:05:51 +01:00
J. Fernando Sánchez
b0eb2e0628 Fixed error with Sentiment140 2015-10-08 19:39:12 +02:00
J. Fernando Sánchez
60415f8217 Added Dockerfile and instructions 2015-10-08 19:26:02 +02:00
J. Fernando Sánchez
724eac38d8 Bumped to 0.4.8 2015-10-08 19:21:24 +02:00
J. Fernando Sánchez
8fa372de15 Added new launch option to the README 2015-10-08 18:58:21 +02:00
J. Fernando Sánchez
a1ffe04a30 More sensible exceptions when importing 2015-10-08 18:23:37 +02:00
J. Fernando Sánchez
74b0cf868e Added console script 2015-10-08 18:20:16 +02:00
J. Fernando Sánchez
50e8e2730b Added default plugins to app.py 2015-10-06 14:39:42 +02:00
J. Fernando Sánchez
b484b453e0 Added indentation and default plugins
* setup.py:
2015-09-29 11:14:28 +02:00
J. Fernando Sánchez
7c2e0ddec7 Added plugins by default and monkey patching
Fixes #2
2015-06-18 17:53:15 +02:00
J. Fernando Sánchez
384aba4654 Sleep plugin sleeps on request too 2015-04-26 21:08:36 +02:00
J. Fernando Sánchez
a857dd3042 Typo in README 2015-04-26 20:47:29 +02:00
J. Fernando Sánchez
b1b672f66d Quick note about using and installing 2015-02-24 18:04:34 +01:00
J. Fernando Sánchez
09d9143a82 Added docs 2015-02-24 09:03:36 +01:00
J. Fernando Sánchez
c1a6b57ac5 Better readme, fixed app.py 2015-02-24 08:43:00 +01:00
J. Fernando Sánchez
6b78b7ccc7 Fixed requirements 2015-02-24 07:54:58 +01:00
J. Fernando Sánchez
f0b1cfcba6 Forcing Travis 2015-02-24 07:50:14 +01:00
J. Fernando Sánchez
4bcd046016 Added TravisCI 2015-02-24 07:44:29 +01:00
J. Fernando Sánchez
ae09f609c2 Improved message when no plugins are found 2015-02-24 07:37:51 +01:00
J. Fernando Sánchez
d1006bbc92 PEP8+Better JSON-LD support
* The API has also changed, there are new parameters to send the
context as part of the headers.
* Improved tests
* PEP8 compliance (despite the line about gevent)
2015-02-24 07:15:25 +01:00
J. Fernando Sánchez
d58137e8f9 Changed to GSI-UPM 2015-02-23 20:22:58 +01:00
J. Fernando Sánchez
79c83e34a3 Added random plugin and other features 2015-02-23 02:13:31 +01:00
J. Fernando Sánchez
37a098109f Module script and improvement in JSON-LD 2014-12-02 13:31:15 +01:00
J. Fernando Sánchez
ff14925056 Improved plugins, better tests, gevent
Moved from Yapsy again (it is not flexible enough), now we use a
custom solution.
The activation and deactivation of plugins is asynchronous, so
that plugins that take a long time don't interfere with the rest.
2014-12-01 18:27:20 +01:00
J. Fernando Sánchez
10f4782ad7 Better NIF compliance 2014-12-01 09:38:23 +01:00
J. Fernando Sánchez
4351f76b60 Removed unnecessary contexts 2014-11-27 17:43:19 +01:00
J. Fernando Sánchez
86f45f8147 JSON-LD contexts and prefixes 2014-11-27 17:39:36 +01:00
J. Fernando Sánchez
2834967026 Better jsonld support 2014-11-27 11:27:05 +01:00
J. Fernando Sánchez
2f7a8d7267 Fixed setup.py and pip 2014-11-20 20:54:57 +01:00
J. Fernando Sánchez
2b68838514 PEP8 compliance 2014-11-20 19:29:49 +01:00
J. Fernando Sánchez
eaf65f0c6b First tests 2014-11-07 19:12:21 +01:00
J. Fernando Sánchez
a5e79bead3 Version 0.2.5 - Pypi 2014-11-05 19:21:17 +01:00
J. Fernando Sánchez
54492472ba Files for deployment 2014-11-05 19:17:27 +01:00
J. Fernando Sánchez
ff8d12074b Improved plugins (reload, imp) 2014-11-04 21:31:41 +01:00
J. Fernando Sánchez
bdf1992775 Fixed plugins 2014-10-17 19:14:49 +02:00
163 changed files with 59172 additions and 506 deletions

9
.drone.yml Normal file
View File

@@ -0,0 +1,9 @@
build:
image: python:$$PYTHON_VERSION
commands:
- python setup.py test
matrix:
PYTHON_VERSION:
- 2.7
- 3.4

10
.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
*.pyc
.*
*egg-info
dist
build
README.html
__pycache__
VERSION
Dockerfile-*
Dockerfile

65
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,65 @@
image: gsiupm/dockermake:latest
# When using dind, it's wise to use the overlayfs driver for
# improved performance.
variables:
DOCKER_DRIVER: overlay
DOCKERFILE: Dockerfile
IMAGENAME: $CI_REGISTRY_IMAGE
stages:
- test
- push
- clean
.test: &test_definition
stage: test
script:
- make -e test-$PYTHON_VERSION
test-3.5:
<<: *test_definition
variables:
PYTHON_VERSION: "3.5"
test-2.7:
<<: *test_definition
variables:
PYTHON_VERSION: "2.7"
.image: &image_definition
stage: push
before_script:
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
script:
- make -e push-$PYTHON_VERSION
only:
- tags
- triggers
push-3.5:
<<: *image_definition
variables:
PYTHON_VERSION: "3.5"
push-2.7:
<<: *image_definition
variables:
PYTHON_VERSION: "2.7"
push-latest:
<<: *image_definition
variables:
PYTHON_VERSION: latest
only:
- master
- triggers
clean :
stage: clean
script:
- make -e clean
only:
- master

5
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,5 @@
- repo: git://github.com/pre-commit/pre-commit-hooks
sha: e626cd57090d8df0be21e4df0f4e55cc3511d6ab
hooks:
- id: flake8
- id: check-json

13
.travis.yml Normal file
View File

@@ -0,0 +1,13 @@
sudo: required
services:
- docker
language: python
env:
- PYV=2.7
- PYV=3.4
- PYV=3.5
# run nosetests - Tests
script: make test-$PYV

22
Dockerfile.template Normal file
View File

@@ -0,0 +1,22 @@
from python:{{PYVERSION}}
MAINTAINER J. Fernando Sánchez <jf.sanchez@upm.es>
RUN mkdir /cache/ /senpy-plugins /data/
VOLUME /data/
ENV PIP_CACHE_DIR=/cache/ SENPY_DATA=/data
ONBUILD COPY . /senpy-plugins/
ONBUILD RUN python -m senpy --only-install -f /senpy-plugins
ONBUILD WORKDIR /senpy-plugins/
WORKDIR /usr/src/app
COPY test-requirements.txt requirements.txt /usr/src/app/
RUN pip install --use-wheel -r test-requirements.txt -r requirements.txt
COPY . /usr/src/app/
RUN pip install --no-deps --no-index .
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]

9
MANIFEST.in Normal file
View File

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

109
Makefile Normal file
View File

@@ -0,0 +1,109 @@
PYVERSIONS=3.5 2.7
PYMAIN=$(firstword $(PYVERSIONS))
NAME=senpy
REPO=gsiupm
VERSION=$(shell git describe --tags --dirty 2>/dev/null)
TARNAME=$(NAME)-$(VERSION).tar.gz
IMAGENAME=$(REPO)/$(NAME)
IMAGEWTAG=$(IMAGENAME):$(VERSION)
action="test-${PYMAIN}"
all: build run
.FORCE:
version: .FORCE
@echo $(VERSION) > $(NAME)/VERSION
@echo $(VERSION)
yapf:
yapf -i -r senpy
yapf -i -r tests
init:
pip install --user pre-commit
pre-commit install
dockerfiles: $(addprefix Dockerfile-,$(PYVERSIONS))
@unlink Dockerfile >/dev/null
ln -s Dockerfile-$(PYMAIN) Dockerfile
Dockerfile-%: Dockerfile.template
sed "s/{{PYVERSION}}/$*/" Dockerfile.template > Dockerfile-$*
quick_build: $(addprefix build-, $(PYMAIN))
build: $(addprefix build-, $(PYVERSIONS))
build-%: version Dockerfile-%
docker build -t '$(IMAGEWTAG)-python$*' -f Dockerfile-$* .;
quick_test: $(addprefix test-,$(PYMAIN))
dev-%:
@docker start $(NAME)-dev$* || (\
$(MAKE) build-$*; \
docker run -d -w /usr/src/app/ -v $$PWD:/usr/src/app --entrypoint=/bin/bash -ti --name $(NAME)-dev$* '$(IMAGEWTAG)-python$*'; \
)\
docker exec -ti $(NAME)-dev$* bash
dev: dev-$(PYMAIN)
test-all: $(addprefix test-,$(PYVERSIONS))
test-%: build-%
docker run --rm --entrypoint /usr/local/bin/python -w /usr/src/app $(IMAGEWTAG)-python$* setup.py test
test: test-$(PYMAIN)
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))
clean:
@docker ps -a | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1;}}' | xargs docker rm -v 2>/dev/null|| true
@docker images | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1":"$$2;}}' | xargs docker rmi 2>/dev/null|| true
@docker rmi $(NAME)-dev 2>/dev/null || true
git_commit:
git commit -a
git_tag:
git tag ${VERSION}
git_push:
git push --tags origin master
pip_upload:
python setup.py sdist upload ;
run-%: build-%
docker run --rm -p 5000:5000 -ti '$(IMAGEWTAG)-python$(PYMAIN)' --default-plugins
run: run-$(PYMAIN)
push-latest: build-$(PYMAIN)
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGEWTAG)'
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGENAME)'
docker push '$(IMAGENAME):latest'
docker push '$(IMAGEWTAG)'
push-%: build-%
docker push $(IMAGENAME):$(VERSION)-python$*
push: $(addprefix push-,$(PYVERSIONS))
docker tag '$(IMAGEWTAG)-python$(PYMAIN)' '$(IMAGEWTAG)'
docker push $(IMAGENAME):$(VERSION)
ci:
gitlab-runner exec docker --docker-volumes /var/run/docker.sock:/var/run/docker.sock --env CI_PROJECT_NAME=$(NAME) ${action}
.PHONY: test test-% test-all build-% build test pip_test run yapf push-main push-% dev ci version .FORCE

View File

@@ -1 +1 @@
web: gunicorn app:app --log-file=-
web: python -m senpy --host 0.0.0.0 --port $PORT --default-plugins

View File

@@ -1,19 +0,0 @@
![GSI Logo](logo.png)
[Senpy](http://senpy.herokuapp.com)
=========================================
Example endpoint that yields results compatible with the EUROSENTIMENT format and exposes the NIF API.
It can be used as a template to adapt existing services to EUROSENTIMENT or to create new services.
[DEMO on Heroku](http://eurosentiment-endpoint.herokuapp.com)
This endpoint serves as bootcampt for any developer wishing to build applications that use the EUROSENTIMENT services.
Acknowledgement
---------------
EUROSENTIMENT PROJECT
Grant Agreement no: 296277
Starting date: 01/09/2012
Project duration: 24 months
![Eurosentiment Logo](logo_grande.png)
![FP7 logo](logo_fp7.gif)

91
README.rst Normal file
View File

@@ -0,0 +1,91 @@
.. image:: img/header.png
:height: 6em
:target: http://demos.gsi.dit.upm.es/senpy
.. image:: https://travis-ci.org/gsi-upm/senpy.svg?branch=master
:target: https://travis-ci.org/gsi-upm/senpy
Senpy lets you create sentiment analysis web services easily, fast and using a well known API.
As a bonus, senpy services use semantic vocabularies (e.g. `NIF <http://persistence.uni-leipzig.org/nlp2rdf/>`_, `Marl <http://www.gsi.dit.upm.es/ontologies/marl>`_, `Onyx <http://www.gsi.dit.upm.es/ontologies/onyx>`_) and formats (turtle, JSON-LD, xml-rdf).
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://senpy.cluster.gsi.dit.upm.es/>`_
Installation
------------
The stable version can be installed in three ways.
Through PIP
***********
.. code:: bash
pip install --user senpy
Alternatively, you can use the development version:
.. code:: bash
git clone http://github.com/gsi-upm/senpy
cd senpy
pip install --user .
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 gsiupm/senpy --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 gsiupm/senpy --default-plugins -f /plugins``
Usage
-----
However, the easiest and recommended way is to just use the command-line tool to load your plugins and launch the server.
.. code:: bash
senpy
or, alternatively:
.. code:: bash
python -m senpy
This will create a server with any modules found in the current path.
For more options, see the `--help` page.
Alternatively, you can use the modules included in senpy to build your own application.
Deploying on Heroku
-------------------
Use a free heroku instance to share your service with the world.
Just use the example Procfile in this repository, or build your own.
`DEMO on heroku <http://senpy.herokuapp.com>`_
For more information, check out the `documentation <http://senpy.readthedocs.org>`_.
------------------------------------------------------------------------------------
Acknowledgement
---------------
This development has been partially funded by the European Union through the MixedEmotions Project (project number H2020 655632), as part of the `RIA ICT 15 Big data and Open Data Innovation and take-up` programme.
.. image:: img/me.png
:target: http://mixedemotions-project.eu
:height: 100px
:alt: MixedEmotions Logo
.. image:: img/eu-flag.jpg
:height: 100px
:target: http://ec.europa.eu/research/participants/portal/desktop/en/opportunities/index.html

33
app.py
View File

@@ -1,33 +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.
'''
Simple Sentiment Analysis server for EUROSENTIMENT
This class shows how to use the nif_server module to create custom services.
'''
import config
from flask import Flask
from senpy import Senpy
app = Flask(__name__)
sp = Senpy()
sp.init_app(app)
if __name__ == '__main__':
app.debug = config.DEBUG
app.run()

1
dev-requirements.txt Normal file
View File

@@ -0,0 +1 @@
mock

1
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
_build

177
docs/Makefile Normal file
View File

@@ -0,0 +1,177 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Senpy.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Senpy.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/Senpy"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Senpy"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

1
docs/_static/schemas vendored Symbolic link
View File

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

192
docs/api.rst Normal file
View File

@@ -0,0 +1,192 @@
NIF API
=======
.. http:get:: /api
Basic endpoint for sentiment/emotion analysis.
**Example request**:
.. sourcecode:: http
GET /api?input=I%20love%20GSI HTTP/1.1
Host: localhost
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"@context": [
"http://127.0.0.1/static/context.jsonld",
],
"analysis": [
{
"@id": "SentimentAnalysisExample",
"@type": "marl:SentimentAnalysis",
"dc:language": "en",
"marl:maxPolarityValue": 10.0,
"marl:minPolarityValue": 0.0
}
],
"domain": "wndomains:electronics",
"entries": [
{
"opinions": [
{
"prov:generatedBy": "SentimentAnalysisExample",
"marl:polarityValue": 7.8,
"marl:hasPolarity": "marl:Positive",
"marl:describesObject": "http://www.gsi.dit.upm.es",
}
],
"nif:isString": "I love GSI",
"strings": [
{
"nif:anchorOf": "GSI",
"nif:taIdentRef": "http://www.gsi.dit.upm.es"
}
]
}
]
}
:query i input: No default. Depends on informat and intype
:query f informat: one of `turtle` (default), `text`, `json-ld`
:query t intype: one of `direct` (default), `url`
:query o outformat: one of `turtle` (default), `text`, `json-ld`
:query p prefix: prefix for the URIs
:query algo algorithm: algorithm/plugin to use for the analysis. For a list of options, see :http:get:`/api/plugins`. If not provided, the default plugin will be used (:http:get:`/api/plugins/default`).
:query algo emotionModel: desired emotion model in the results. If the requested algorithm does not use that emotion model, there are conversion plugins specifically for this. If none of the plugins match, an error will be returned, which includes the results *as is*.
:reqheader Accept: the response content type depends on
:mailheader:`Accept` header
:resheader Content-Type: this depends on :mailheader:`Accept`
header of request
:statuscode 200: no error
:statuscode 404: service not found
:statuscode 400: error while processing the request
.. http:post:: /api
The same as :http:get:`/api`.
.. http:get:: /api/plugins
Returns a list of installed plugins.
**Example request**:
.. sourcecode:: http
GET /api/plugins HTTP/1.1
Host: localhost
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
{
"@context": {
...
},
"@type": "plugins",
"plugins": [
{
"name": "sentiment140",
"is_activated": true,
"version": "0.1",
"extra_params": {
"@id": "extra_params_sentiment140_0.1",
"language": {
"required": false,
"@id": "lang_sentiment140",
"options": [
"es",
"en",
"auto"
],
"aliases": [
"language",
"l"
]
}
},
"@id": "sentiment140_0.1"
}, {
"name": "rand",
"is_activated": true,
"version": "0.1",
"extra_params": {
"@id": "extra_params_rand_0.1",
"language": {
"required": false,
"@id": "lang_rand",
"options": [
"es",
"en",
"auto"
],
"aliases": [
"language",
"l"
]
}
},
"@id": "rand_0.1"
}
]
}
.. http:get:: /api/plugins/<pluginname>
Returns the information of a specific plugin.
**Example request**:
.. sourcecode:: http
GET /api/plugins/rand/ HTTP/1.1
Host: localhost
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
{
"@id": "rand_0.1",
"@type": "sentimentPlugin",
"extra_params": {
"@id": "extra_params_rand_0.1",
"language": {
"@id": "lang_rand",
"aliases": [
"language",
"l"
],
"options": [
"es",
"en",
"auto"
],
"required": false
}
},
"is_activated": true,
"name": "rand",
"version": "0.1"
}
.. http:get:: /api/plugins/default
Return the information about the default plugin.

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"
}
]
}

274
docs/conf.py Normal file
View File

@@ -0,0 +1,274 @@
# -*- coding: utf-8 -*-
# flake8: noqa
#
# Senpy documentation build configuration file, created by
# sphinx-quickstart on Tue Feb 24 08:57:32 2015.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.doctest',
'sphinx.ext.todo',
'sphinxcontrib.httpdomain',
'sphinx.ext.coverage',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Senpy'
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.
with open('../senpy/VERSION') as f:
version = f.read().strip()
# The full version, including alpha/beta/rc tags.
release = version
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# -- Options for HTML output ----------------------------------------------
if not on_rtd: # only import and set the theme if we're building docs locally
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
else:
html_theme = 'default'
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'Senpydoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'Senpy.tex', u'Senpy Documentation',
u'J. Fernando Sánchez', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'senpy', u'Senpy Documentation',
[u'J. Fernando Sánchez'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'Senpy', u'Senpy Documentation',
u'J. Fernando Sánchez', 'Senpy', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False

116
docs/conversion.rst Normal file
View File

@@ -0,0 +1,116 @@
Conversion
----------
Senpy includes experimental support for emotion/sentiment conversion plugins.
Use
===
Consider the original query: http://127.0.0.1:5000/api/?i=hello&algo=emoRand
The requested plugin (emoRand) returns emotions using Ekman's model (or big6 in EmotionML):
.. code:: json
... rest of the document ...
{
"@type": "emotionSet",
"onyx:hasEmotion": {
"@type": "emotion",
"onyx:hasEmotionCategory": "emoml:big6anger"
},
"prov:wasGeneratedBy": "plugins/emoRand_0.1"
}
To get these emotions in VAD space (FSRE dimensions in EmotionML), we'd do this:
http://127.0.0.1:5000/api/?i=hello&algo=emoRand&emotionModel=emoml:fsre-dimensions
This call, provided there is a valid conversion plugin from Ekman's to VAD, would return something like this:
.. code:: json
... rest of the document ...
{
"@type": "emotionSet",
"onyx:hasEmotion": {
"@type": "emotion",
"onyx:hasEmotionCategory": "emoml:big6anger"
},
"prov:wasGeneratedBy": "plugins/emoRand_0.1"
}, {
"@type": "emotionSet",
"onyx:hasEmotion": {
"@type": "emotion",
"A": 7.22,
"D": 6.28,
"V": 8.6
},
"prov:wasGeneratedBy": "plugins/Ekman2VAD_0.1"
}
That is called a *full* response, as it simply adds the converted emotion alongside.
It is also possible to get the original emotion nested within the new converted emotion, using the `conversion=nested` parameter:
.. code:: json
... rest of the document ...
{
"@type": "emotionSet",
"onyx:hasEmotion": {
"@type": "emotion",
"onyx:hasEmotionCategory": "emoml:big6anger"
},
"prov:wasGeneratedBy": "plugins/emoRand_0.1"
"onyx:wasDerivedFrom": {
"@type": "emotionSet",
"onyx:hasEmotion": {
"@type": "emotion",
"A": 7.22,
"D": 6.28,
"V": 8.6
},
"prov:wasGeneratedBy": "plugins/Ekman2VAD_0.1"
}
}
Lastly, `conversion=filtered` would only return the converted emotions.
Developing a conversion plugin
================================
Conversion plugins are discovered by the server just like any other plugin.
The difference is the slightly different API, and the need to specify the `source` and `target` of the conversion.
For instance, an emotion conversion plugin needs the following:
.. code:: yaml
---
onyx:doesConversion:
- onyx:conversionFrom: emoml:big6
onyx:conversionTo: emoml:fsre-dimensions
- onyx:conversionFrom: emoml:fsre-dimensions
onyx:conversionTo: emoml:big6
.. code:: python
class MyConversion(EmotionConversionPlugin):
def convert(self, emotionSet, fromModel, toModel, params):
pass

75
docs/demo.rst Normal file
View File

@@ -0,0 +1,75 @@
Demo
----
There is a demo available on http://senpy.demos.gsi.dit.upm.es/, where you can a serie of different plugins. You can use them in the playground or make a directly requests to the service.
.. image:: senpy-playground.png
:height: 400px
:width: 800px
:scale: 100 %
:align: center
Plugins Demo
============
The next plugins are available at the demo:
* emoTextAnew extracts the VAD (valence-arousal-dominance) of a sentence by matching words from the ANEW dictionary.
* emoTextWordnetAffect based on the hierarchy of WordnetAffect to calculate the emotion of the sentence.
* vaderSentiment utilizes the software from vaderSentiment to calculate the sentiment of a sentence.
* sentiText is a software developed during the TASS 2015 competition, it has been adapted for English and Spanish.
emoTextANEW plugin
******************
This plugin is going to used the ANEW lexicon dictionary to calculate de VAD (valence-arousal-dominance) of the sentence and the determinate which emotion is closer to this value.
Each emotion has a centroid, which it has been approximated using the formula described in this article:
http://www.aclweb.org/anthology/W10-0208
The plugin is going to look for the words in the sentence that appear in the ANEW dictionary and calculate the average VAD score for the sentence. Once this score is calculated, it is going to seek the emotion that is closest to this value.
emoTextWAF plugin
*****************
This plugin uses WordNet-Affect (http://wndomains.fbk.eu/wnaffect.html) to calculate the percentage of each emotion. The emotions that are going to be used are: anger, fear, disgust, joy and sadness. It is has been used a emotion mapping enlarge the emotions:
* anger : general-dislike
* fear : negative-fear
* disgust : shame
* joy : gratitude, affective, enthusiasm, love, joy, liking
* sadness : ingrattitude, daze, humlity, compassion, despair, anxiety, sadness
sentiText plugin
****************
This plugin is based in the classifier developed for the TASS 2015 competition. It has been developed for Spanish and English. The different phases that has this plugin when it is activated:
* Train both classifiers (English and Spanish).
* Initialize resources (dictionaries,stopwords,etc.).
* Extract bag of words,lemmas and chars.
Once the plugin is activated, the features that are going to be extracted for the classifiers are:
* Matches with the bag of words extracted from the train corpus.
* Sentiment score of the sentences extracted from the dictionaries (lexicons and emoticons).
* Identify negations and intensifiers in the sentences.
* Complementary features such as exclamation and interrogation marks, eloganted and caps words, hashtags, etc.
The plugin has a preprocessor, which is focues on Twitter corpora, that is going to be used for cleaning the text to simplify the feature extraction.
There is more information avaliable in the next article.
Aspect based Sentiment Analysis of Spanish Tweets, Oscar Araque and Ignacio Corcuera-Platas and Constantino Román-Gómez and Carlos A. Iglesias and J. Fernando Sánchez-Rada. http://gsi.dit.upm.es/es/investigacion/publicaciones?view=publication&task=show&id=37
vaderSentiment plugin
*********************
For developing this plugin, it has been used the module vaderSentiment, which is described in the paper: VADER: A Parsimonious Rule-based Model for Sentiment Analysis of Social Media Text C.J. Hutto and Eric Gilbert Eighth International Conference on Weblogs and Social Media (ICWSM-14). Ann Arbor, MI, June 2014.
If you use this plugin in your research, please cite the above paper
For more information about the functionality, check the official repository
https://github.com/cjhutto/vaderSentiment

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,42 @@
{
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
"@id": "me:Result1",
"@type": "results",
"analysis": [
{
"@id": "me:EmotionAnalysis1",
"@type": "onyx:EmotionAnalysis"
}
],
"entries": [
{
"@id": "http://micro.blog/status1",
"@type": [
"nif:RFC5147String",
"nif:Context"
],
"nif:isString": "Dear Microsoft, put your Windows Phone on your newest #open technology program. You'll be awesome. #opensource",
"entities": [
],
"suggestions": [
],
"sentiments": [
],
"emotions": [
{
"@id": "http://micro.blog/status1#char=0,109",
"nif:anchorOf": "Dear Microsoft, put your Windows Phone on your newest #open technology program. You'll be awesome. #opensource",
"prov:wasGeneratedBy": "me:EmotionAnalysis1",
"onyx:hasEmotion": [
{
"onyx:hasEmotionCategory": "wna:liking"
},
{
"onyx:hasEmotionCategory": "wna:excitement"
}
]
}
]
}
]
}

View File

@@ -0,0 +1,45 @@
{
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
"@id": "me:Result1",
"@type": "results",
"analysis": [
{
"@id": "me:NER1",
"@type": "me:NERAnalysis"
}
],
"entries": [
{
"@id": "http://micro.blog/status1",
"@type": [
"nif:RFC5147String",
"nif:Context"
],
"nif:isString": "Dear Microsoft, put your Windows Phone on your newest #open technology program. You'll be awesome. #opensource",
"entities": [
{
"@id": "http://micro.blog/status1#char=5,13",
"nif:beginIndex": 5,
"nif:endIndex": 13,
"nif:anchorOf": "Microsoft",
"me:references": "http://dbpedia.org/page/Microsoft",
"prov:wasGeneratedBy": "me:NER1"
},
{
"@id": "http://micro.blog/status1#char=25,37",
"nif:beginIndex": 25,
"nif:endIndex": 37,
"nif:anchorOf": "Windows Phone",
"me:references": "http://dbpedia.org/page/Windows_Phone",
"prov:wasGeneratedBy": "me:NER1"
}
],
"suggestions": [
],
"sentiments": [
],
"emotionSets": [
]
}
]
}

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

@@ -0,0 +1,40 @@
{
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
"@id": "me:Result1",
"@type": "results",
"analysis": [
{
"@id": "me:SAnalysis1",
"@type": "marl:SentimentAnalysis",
"marl:maxPolarityValue": 1,
"marl:minPolarityValue": 0
}
],
"entries": [
{
"@id": "http://micro.blog/status1",
"@type": [
"nif:RFC5147String",
"nif:Context"
],
"nif:isString": "Dear Microsoft, put your Windows Phone on your newest #open technology program. You'll be awesome. #opensource",
"entities": [
],
"suggestions": [
],
"sentiments": [
{
"@id": "http://micro.blog/status1#char=80,97",
"nif:beginIndex": 80,
"nif:endIndex": 97,
"nif:anchorOf": "You'll be awesome.",
"marl:hasPolarity": "marl:Positive",
"marl:polarityValue": 0.9,
"prov:wasGeneratedBy": "me:SAnalysis1"
}
],
"emotionSets": [
]
}
]
}

View File

@@ -0,0 +1,37 @@
{
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
"@id": "me:Result1",
"@type": "results",
"analysis": [
{
"@id": "me:SgAnalysis1",
"@type": "me:SuggestionAnalysis"
}
],
"entries": [
{
"@id": "http://micro.blog/status1",
"@type": [
"nif:RFC5147String",
"nif:Context"
],
"prov:wasGeneratedBy": "me:SAnalysis1",
"nif:isString": "Dear Microsoft, put your Windows Phone on your newest #open technology program. You'll be awesome. #opensource",
"entities": [
],
"suggestions": [
{
"@id": "http://micro.blog/status1#char=16,77",
"nif:beginIndex": 16,
"nif:endIndex": 77,
"nif:anchorOf": "put your Windows Phone on your newest #open technology program",
"prov:wasGeneratedBy": "me:SgAnalysis1"
}
],
"sentiments": [
],
"emotionSets": [
]
}
]
}

15
docs/index.rst Normal file
View File

@@ -0,0 +1,15 @@
Welcome to Senpy's documentation!
=================================
Contents:
.. toctree::
senpy
installation
usage
api
schema
plugins
conversion
demo
:maxdepth: 2

27
docs/installation.rst Normal file
View File

@@ -0,0 +1,27 @@
Installation
------------
The stable version can be installed in three ways.
Through PIP
***********
.. code:: bash
pip install --user senpy
Alternatively, you can use the development version:
.. code:: bash
git clone git@github.com:gsi-upm/senpy
cd senpy
pip install --user .
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 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 gsiupm/senpy --host 0.0.0.0 --default-plugins -f /plugins``

248
docs/plugins.rst Normal file
View File

@@ -0,0 +1,248 @@
Developing new plugins
----------------------
This document describes how to develop a new analysis plugin. For an example of conversion plugins, see :doc:`conversion`.
Each plugin represents a different analysis process.There are two types of files that are needed by senpy for loading a plugin:
- Definition file, has the ".senpy" extension.
- Code file, is a python file.
This separation will allow us to deploy plugins that use the same code but employ different parameters.
For instance, one could use the same classifier and processing in several plugins, but train with different datasets.
This scenario is particularly useful for evaluation purposes.
The only limitation is that the name of each plugin needs to be unique.
Plugins Definitions
===================
The definition file contains all the attributes of the plugin, and can be written in YAML or JSON.
The most important attributes are:
* **name**: unique name that senpy will use internally to identify the plugin.
* **module**: indicates the module that contains the plugin code, which will be automatically loaded by senpy.
* **version**
* extra_params: used to specify parameters that the plugin accepts that are not already part of the senpy API. Those parameters may be required, and have aliased names. For instance:
.. code:: yaml
extra_params:
hello_param:
aliases: # required
- hello_param
- hello
required: true
default: Hi you
values:
- Hi you
- Hello y'all
- Howdy
Parameter validation will fail if a required parameter without a default has not been provided, or if the definition includes a set of values and the provided one does not match one of them.
A complete example:
.. code:: yaml
name: <Name of the plugin>
module: <Python file>
version: 0.1
And the json equivalent:
.. code:: json
{
"name": "<Name of the plugin>",
"module": "<Python file>",
"version": "0.1"
}
Plugins Code
============
The basic methods in a plugin are:
* __init__
* activate: used to load memory-hungry resources
* deactivate: used to free up resources
* analyse_entry: called in every user requests. It takes in the parameters supplied by a user and should yield one or more ``Entry`` objects.
Plugins are loaded asynchronously, so don't worry if the activate method takes too long. The plugin will be marked as activated once it is finished executing the method.
Example plugin
==============
In this section, we will implement a basic sentiment analysis plugin.
To determine the polarity of each entry, the plugin will compare the length of the string to a threshold.
This threshold will be included in the definition file.
The definition file would look like this:
.. code:: yaml
name: helloworld
module: helloworld
version: 0.0
threshold: 10
Now, in a file named ``helloworld.py``:
.. code:: python
#!/bin/env python
#helloworld.py
from senpy.plugins import SenpyPlugin
from senpy.models import Sentiment
class HelloWorld(SenpyPlugin):
def analyse_entry(entry, params):
'''Basically do nothing with each entry'''
sentiment = Sentiment()
if len(entry.text) < self.threshold:
sentiment['marl:hasPolarity'] = 'marl:Positive'
else:
sentiment['marl:hasPolarity'] = 'marl:Negative'
entry.sentiments.append(sentiment)
yield entry
F.A.Q.
======
Why does the analyse function yield instead of return?
??????????????????????????????????????????????????????
This is so that plugins may add new entries to the response or filter some of them.
For instance, a `context detection` plugin may add a new entry for each context in the original entry.
On the other hand, a conveersion plugin may leave out those entries that do not contain relevant information.
If I'm using a classifier, where should I train it?
???????????????????????????????????????????????????
Training a classifier can be time time consuming. To avoid running the training unnecessarily, you can use ShelfMixin to store the classifier. For instance:
.. code:: python
from senpy.plugins import ShelfMixin, SenpyPlugin
class MyPlugin(ShelfMixin, SenpyPlugin):
def train(self):
''' Code to train the classifier
'''
# Here goes the code
# ...
return classifier
def activate(self):
if 'classifier' not in self.sh:
classifier = self.train()
self.sh['classifier'] = classifier
self.classifier = self.sh['classifier']
def deactivate(self):
self.close()
You can speficy a 'shelf_file' in your .senpy file. By default the ShelfMixin creates a file based on the plugin name and stores it in that plugin's folder.
I want to implement my service as a plugin, How i can do it?
????????????????????????????????????????????????????????????
This example ilustrate how to implement the Sentiment140 service as a plugin in senpy
.. code:: python
class Sentiment140Plugin(SentimentPlugin):
def analyse_entry(self, entry, params):
text = entry.text
lang = params.get("language", "auto")
res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson",
json.dumps({"language": lang,
"data": [{"text": text}]
}
)
)
p = params.get("prefix", None)
polarity_value = self.maxPolarityValue*int(res.json()["data"][0]
["polarity"]) * 0.25
polarity = "marl:Neutral"
neutral_value = self.maxPolarityValue / 2.0
if polarity_value > neutral_value:
polarity = "marl:Positive"
elif polarity_value < neutral_value:
polarity = "marl:Negative"
sentiment = Sentiment(id="Sentiment0",
prefix=p,
marl__hasPolarity=polarity,
marl__polarityValue=polarity_value)
sentiment.prov__wasGeneratedBy = self.id
entry.sentiments.append(sentiment)
yield entry
Where can I define extra parameters to be introduced in the request to my plugin?
?????????????????????????????????????????????????????????????????????????????????
You can add these parameters in the definition file under the attribute "extra_params" : "{param_name}". The name of the parameter has new attributes-value pairs. The basic attributes are:
* aliases: the different names which can be used in the request to use the parameter.
* required: this option is a boolean and indicates if the parameters is binding in operation plugin.
* options: the different values of the paremeter.
* default: the default value of the parameter, this is useful in case the paremeter is required and you want to have a default value.
.. code:: python
"extra_params": {
"language": {
"aliases": ["language", "l"],
"required": true,
"options": ["es","en"],
"default": "es"
}
}
This example shows how to introduce a parameter associated with language.
The extraction of this paremeter is used in the analyse method of the Plugin interface.
.. code:: python
lang = params.get("language")
Where can I set up variables for using them in my plugin?
?????????????????????????????????????????????????????????
You can add these variables in the definition file with the structure of attribute-value pairs.
Every field added to the definition file is available to the plugin instance.
Can I activate a DEBUG mode for my plugin?
???????????????????????????????????????????
You can activate the DEBUG mode by the command-line tool using the option -d.
.. code:: bash
senpy -d
Additionally, with the ``--pdb`` option you will be dropped into a pdb post mortem shell if an exception is raised.
.. code:: bash
senpy --pdb
Where can I find more code examples?
????????????????????????????????????
See: `<http://github.com/gsi-upm/senpy-plugins-community>`_.

1
docs/requirements.txt Normal file
View File

@@ -0,0 +1 @@
sphinxcontrib-httpdomain>=1.4

74
docs/schema.rst Normal file
View File

@@ -0,0 +1,74 @@
Schema Examples
===============
All the examples in this page use the :download:`the main schema <_static/schemas/definitions.json>`.
Simple NIF annotation
---------------------
Description
...........
This example covers the basic example in the NIF documentation: `<http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core/nif-core.html>`_.
Representation
..............
.. literalinclude:: examples/example-basic.json
:language: json-ld
Sentiment Analysis
---------------------
Description
...........
Representation
..............
.. literalinclude:: examples/example-sentiment.json
:emphasize-lines: 5-10,25-33
:language: json-ld
Suggestion Mining
-----------------
Description
...........
Representation
..............
.. literalinclude:: examples/example-suggestion.json
:emphasize-lines: 5-8,22-27
:language: json-ld
Emotion Analysis
----------------
Description
...........
Representation
..............
.. literalinclude:: examples/example-emotion.json
:language: json-ld
:emphasize-lines: 5-8,25-37
Named Entity Recognition
------------------------
Description
...........
Representation
..............
.. literalinclude:: examples/example-ner.json
:emphasize-lines: 5-8,19-34
:language: json-ld
Complete example
----------------
Description
...........
This example covers all of the above cases, integrating all the annotations in the same document.
Representation
..............
.. literalinclude:: examples/example-complete.json
:language: json-ld

BIN
docs/senpy-architecture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
docs/senpy-playground.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

35
docs/senpy.rst Normal file
View File

@@ -0,0 +1,35 @@
What is Senpy?
--------------
Senpy is an open source reference implementation of a linked data model for sentiment and emotion analysis services based on the vocabularies NIF, Marl and Onyx.
The overall goal of the reference implementation Senpy is easing the adoption of the proposed linked data model for sentiment and emotion analysis services, so that services from different providers become interoperable. With this aim, the design of the reference implementation has focused on its extensibility and reusability.
A modular approach allows organizations to replace individual components with custom ones developed in-house. Furthermore, organizations can benefit from reusing prepackages modules that provide advanced functionalities, such as algorithms for sentiment and emotion analysis, linked data publication or emotion and sentiment mapping between different providers.
Specifications
==============
The model used in Senpy is based on the following specifications:
* Marl, a vocabulary designed to annotate and describe subjetive opinions expressed on the web or in information systems.
* Onyx, which is built one the same principles as Marl to annotate and describe emotions, and provides interoperability with Emotion Markup Language.
* NIF 2.0, which defines a semantic format and APO for improving interoperability among natural language processing services
Architecture
============
The main component of a sentiment analysis service is the algorithm itself. However, for the algorithm to work, it needs to get the appropriate parameters from the user, format the results according to the defined API, interact with the user whn errors occur or more information is needed, etc.
Senpy proposes a modular and dynamic architecture that allows:
* Implementing different algorithms in a extensible way, yet offering a common interface.
* Offering common services that facilitate development, so developers can focus on implementing new and better algorithms.
The framework consists of two main modules: Senpy core, which is the building block of the service, and Senpy plugins, which consist of the analysis algorithm. The next figure depicts a simplified version of the processes involved in an analysis with the Senpy framework.
.. image:: senpy-architecture.png
:height: 400px
:width: 800px
:scale: 100 %
:align: center

80
docs/usage.rst Normal file
View File

@@ -0,0 +1,80 @@
Usage
-----
The easiest and recommended way is to just use the command-line tool to load your plugins and launch the server.
.. code:: bash
senpy
Or, alternatively:
.. code:: bash
python -m senpy
This will create a server with any modules found in the current path.
Useful command-line options
===========================
In case you want to load modules, which are located in different folders under the root folder, use the next option.
.. code:: bash
python -m senpy -f .
The default port used by senpy is 5000, but you can change it using the option `--port`.
.. code:: bash
python -m senpy --port 8080
Also, the host can be changed where senpy is deployed. The default value is `127.0.0.1`.
.. code:: bash
python -m senpy --host 0.0.0.0
For more options, see the `--help` page.
Alternatively, you can use the modules included in senpy to build your own application.
Senpy server
============
Once the server is launched, there is a basic endpoint in the server, which provides a playground to use the plugins that have been loaded.
In case you want to know the different endpoints of the server, there is more information available in the NIF API section_.
CLI
===
This video shows how to use senpy through command-line tool.
https://asciinema.org/a/9uwef1ghkjk062cw2t4mhzpyk
Request example in python
=========================
This example shows how to make a request to the default plugin:
.. code:: python
from senpy.client import Client
c = Client('http://127.0.0.1:5000/api/')
r = c.analyse('hello world')
for entry in r.entries:
print('{} -> {}'.format(entry.text, entry.emotions))
.. _section: http://senpy.readthedocs.org/en/latest/api.html
Conversion
==========
See :doc:`conversion`

BIN
img/eu-flag.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

828
img/final-logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 81 KiB

BIN
img/gsi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
img/header.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

2728
img/logo.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 180 KiB

BIN
img/logo_grande.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
img/me.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

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

View File

@@ -14,19 +14,15 @@
# 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.
'''
"""
Sentiment analysis server in Python
'''
"""
from .version import __version__
import extensions
import blueprints
import plugins
import logging
logger = logging.getLogger(__name__)
if __name__ == '__main__':
from flask import Flask
app = Flask(__name__)
sp = extensions.Senpy()
sp.init_app(app)
app.debug = config.DEBUG
app.run()
logger.info('Using senpy version: {}'.format(__version__))
__all__ = ['api', 'blueprints', 'cli', 'extensions', 'models', 'plugins']

View File

@@ -1,7 +1,123 @@
#!/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.
"""
Senpy is a modular sentiment analysis server. This script runs an instance of
the server.
"""
from flask import Flask
from extensions import Senpy
app = Flask(__name__)
sp = Senpy()
sp.init_app(app)
app.debug = True
app.run()
from senpy.extensions import Senpy
from gevent.wsgi import WSGIServer
from gevent.monkey import patch_all
import logging
import os
import sys
import argparse
import senpy
patch_all(thread=False)
SERVER_PORT = os.environ.get("PORT", 5000)
def info(type, value, tb):
if hasattr(sys, 'ps1') or not sys.stderr.isatty():
# we are in interactive mode or we don't have a tty-like
# device, so we call the default hook
sys.__excepthook__(type, value, tb)
else:
import traceback
import pdb
# we are NOT in interactive mode, print the exception...
traceback.print_exception(type, value, tb)
print
# ...then start the debugger in post-mortem mode.
# pdb.pm() # deprecated
pdb.post_mortem(tb) # more "modern"
def main():
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="0.0.0.0",
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()
rl.setLevel(getattr(logging, args.level))
app = Flask(__name__)
app.debug = args.debug
if args.debug:
sys.excepthook = info
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:
print('Senpy version {}'.format(senpy.__version__))
print('Server running on port %s:%d. Ctrl+C to quit' % (args.host,
args.port))
http_server.serve_forever()
except KeyboardInterrupt:
print('Bye!')
http_server.stop()
sp.deactivate_all()
if __name__ == '__main__':
main()

131
senpy/api.py Normal file
View File

@@ -0,0 +1,131 @@
from future.utils import iteritems
from .models import Error
import logging
logger = logging.getLogger(__name__)
API_PARAMS = {
"algorithm": {
"aliases": ["algorithm", "a", "algo"],
"required": False,
},
"outformat": {
"@id": "outformat",
"aliases": ["outformat", "o"],
"default": "json-ld",
"required": True,
"options": ["json-ld", "turtle"],
},
"expanded-jsonld": {
"@id": "expanded-jsonld",
"aliases": ["expanded", "expanded-jsonld"],
"required": True,
"default": 0
},
"emotionModel": {
"@id": "emotionModel",
"aliases": ["emotionModel", "emoModel"],
"required": False
},
"plugin_type": {
"@id": "pluginType",
"description": 'What kind of plugins to list',
"aliases": ["pluginType", "plugin_type"],
"required": True,
"default": "analysisPlugin"
},
"conversion": {
"@id": "conversion",
"description": "How to show the elements that have (not) been converted",
"required": True,
"options": ["filtered", "nested", "full"],
"default": "full"
}
}
WEB_PARAMS = {
"inHeaders": {
"aliases": ["inHeaders", "headers"],
"required": True,
"default": "0"
},
}
CLI_PARAMS = {
"plugin_folder": {
"aliases": ["plugin_folder", "folder"],
"required": True,
"default": "."
},
}
NIF_PARAMS = {
"input": {
"@id": "input",
"aliases": ["i", "input"],
"required": True,
"help": "Input text"
},
"informat": {
"@id": "informat",
"aliases": ["f", "informat"],
"required": False,
"default": "text",
"options": ["turtle", "text"],
},
"intype": {
"@id": "intype",
"aliases": ["intype", "t"],
"required": False,
"default": "direct",
"options": ["direct", "url", "file"],
},
"language": {
"@id": "language",
"aliases": ["language", "l"],
"required": False,
},
"prefix": {
"@id": "prefix",
"aliases": ["prefix", "p"],
"required": True,
"default": "",
},
"urischeme": {
"@id": "urischeme",
"aliases": ["urischeme", "u"],
"required": False,
"default": "RFC5147String",
"options": "RFC5147String"
},
}
def parse_params(indict, spec=NIF_PARAMS):
logger.debug("Parsing: {}\n{}".format(indict, spec))
outdict = indict.copy()
wrong_params = {}
for param, options in iteritems(spec):
if param[0] != "@": # Exclude json-ld properties
for alias in options.get("aliases", []):
if alias in indict:
outdict[param] = indict[alias]
if param not in outdict:
if options.get("required", False) and "default" not in options:
wrong_params[param] = spec[param]
else:
if "default" in options:
outdict[param] = options["default"]
else:
if "options" in spec[param] and \
outdict[param] not in spec[param]["options"]:
wrong_params[param] = spec[param]
if wrong_params:
logger.debug("Error parsing: %s", wrong_params)
message = Error(
status=400,
message="Missing or invalid parameters",
parameters=outdict,
errors={param: error
for param, error in iteritems(wrong_params)})
raise message
return outdict

View File

@@ -1,152 +1,142 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright 2014 J. Fernando Sánchez Rada - Grupo de Sistemas Inteligentes
# DIT, UPM
# 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
# 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
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# 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.
'''
Simple Sentiment Analysis server
'''
from flask import Blueprint, render_template, request, jsonify, current_app
import json
"""
Blueprints for Senpy
"""
from flask import (Blueprint, request, current_app, render_template, url_for,
jsonify)
from .models import Error, Response, Plugins, read_schema
from .api import WEB_PARAMS, API_PARAMS, parse_params
from .version import __version__
from functools import wraps
nif_blueprint = Blueprint("NIF Sentiment Analysis Server", __name__)
import logging
PARAMS = {"input": {"aliases": ["i", "input"],
"required": True,
"help": "Input text"
},
"informat": {"aliases": ["f", "informat"],
"required": False,
"default": "text",
"options": ["turtle", "text"],
},
"intype": {"aliases": ["intype", "t"],
"required": False,
"default": "direct",
"options": ["direct", "url", "file"],
},
"outformat": {"aliases": ["outformat", "o"],
"default": "json-ld",
"required": False,
"options": ["json-ld"],
},
"algorithm": {"aliases": ["algorithm", "a", "algo"],
"required": False,
},
"language": {"aliases": ["language", "l"],
"required": False,
"options": ["es", "en"],
},
"urischeme": {"aliases": ["urischeme", "u"],
"required": False,
"default": "RFC5147String",
"options": "RFC5147String"
},
}
logger = logging.getLogger(__name__)
def get_algorithm(req):
return get_params(req, params={"algorithm": PARAMS["algorithm"]})
api_blueprint = Blueprint("api", __name__)
demo_blueprint = Blueprint("demo", __name__)
ns_blueprint = Blueprint("ns", __name__)
def get_params(req, params=PARAMS):
indict = None
def get_params(req):
if req.method == 'POST':
indict = req.form
indict = req.form.to_dict(flat=True)
elif req.method == 'GET':
indict = req.args
indict = req.args.to_dict(flat=True)
else:
raise ValueError("Invalid data")
raise Error(message="Invalid data")
return indict
outdict = {}
wrongParams = {}
for param, options in params.iteritems():
for alias in options["aliases"]:
if alias in indict:
outdict[param] = indict[alias]
if param not in outdict:
if options.get("required", False):
wrongParams[param] = params[param]
@demo_blueprint.route('/')
def index():
return render_template("index.html", version=__version__)
@api_blueprint.route('/contexts/<entity>.jsonld')
def context(entity="context"):
context = Response._context
context['@vocab'] = url_for('ns.index', _external=True)
return jsonify({"@context": context})
@ns_blueprint.route('/') # noqa: F811
def index():
context = Response._context
context['@vocab'] = url_for('.ns', _external=True)
return jsonify({"@context": context})
@api_blueprint.route('/schemas/<schema>')
def schema(schema="definitions"):
try:
return jsonify(read_schema(schema))
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):
raw_params = get_params(request)
headers = {'X-ORIGINAL-PARAMS': raw_params}
# Get defaults
web_params = parse_params({}, spec=WEB_PARAMS)
api_params = parse_params({}, spec=API_PARAMS)
outformat = 'json-ld'
try:
print('Getting request:')
print(request)
web_params = parse_params(raw_params, spec=WEB_PARAMS)
api_params = parse_params(raw_params, spec=API_PARAMS)
if hasattr(request, 'params'):
request.params.update(api_params)
else:
if "default" in options:
outdict[param] = options["default"]
else:
if "options" in params[param] and \
outdict[param] not in params[param]["options"]:
wrongParams[param] = params[param]
if wrongParams:
message = {"status": "failed", "message": "Missing or invalid parameters"}
message["parameters"] = outdict
message["errors"] = {param:error for param, error in wrongParams.iteritems()}
raise ValueError(json.dumps(message))
return outdict
request.params = api_params
response = f(*args, **kwargs)
except Error as ex:
response = ex
def basic_analysis(params):
response = {"@context": ["http://demos.gsi.dit.upm.es/eurosentiment/static/context.jsonld",
{
"@base": "{}#".format(request.url.encode('utf-8'))
}
],
"analysis": [{
"@type": "marl:SentimentAnalysis"
}],
"entries": []
}
if "language" in params:
response["language"] = params["language"]
for idx, sentence in enumerate(params["input"].split(".")):
response["entries"].append({
"@id": "Sentence{}".format(idx),
"nif:isString": sentence
})
in_headers = web_params['inHeaders'] != "0"
expanded = api_params['expanded-jsonld']
outformat = api_params['outformat']
return response.flask(
in_headers=in_headers,
headers=headers,
prefix=url_for('.api', _external=True),
context_uri=url_for('api.context',
entity=type(response).__name__,
_external=True),
outformat=outformat,
expanded=expanded)
return decorated_function
@api_blueprint.route('/', methods=['POST', 'GET'])
@basic_api
def api():
response = current_app.senpy.analyse(**request.params)
return response
@nif_blueprint.route('/', methods=['POST', 'GET'])
def home(entries=None):
try:
algo = get_algorithm(request)["algorithm"]
specific_params = PARAMS.copy()
specific_params.update(current_app.senpy.parameters(algo))
params = get_params(request, specific_params)
except ValueError as ex:
return ex.message
response = current_app.senpy.analyse(**params)
return jsonify(response)
@nif_blueprint.route('/plugins/', methods=['POST', 'GET'])
@nif_blueprint.route('/plugins/<plugin>', methods=['POST', 'GET'])
@nif_blueprint.route('/plugins/<plugin>/<action>', methods=['POST', 'GET'])
def plugins(plugin=None, action="list"):
print current_app.senpy.plugins.keys()
if plugin:
plugs = {plugin:current_app.senpy.plugins[plugin]}
else:
plugs = current_app.senpy.plugins
if action == "list":
dic = {plug:plugs[plug].jsonable(True) for plug in plugs}
return jsonify(dic)
elif action == "disable":
plugs[plugin].enabled = False
return "Ok"
elif action == "enable":
plugs[plugin].enabled = True
return "Ok"
else:
return "action '{}' not allowed".format(action), 404
@api_blueprint.route('/plugins/', methods=['POST', 'GET'])
@basic_api
def plugins():
sp = current_app.senpy
ptype = request.params.get('plugin_type')
plugins = sp.filter_plugins(plugin_type=ptype)
dic = Plugins(plugins=list(plugins.values()))
return dic
if __name__ == '__main__':
import config
from flask import Flask
app = Flask(__name__)
app.register_blueprint(nif_blueprint)
app.debug = config.DEBUG
app.run()
@api_blueprint.route('/plugins/<plugin>/', methods=['POST', 'GET'])
@basic_api
def plugin(plugin=None):
sp = current_app.senpy
if plugin == 'default' and sp.default_plugin:
return sp.default_plugin
plugins = sp.filter_plugins(
id='plugins/{}'.format(plugin)) or sp.filter_plugins(name=plugin)
if plugins:
response = list(plugins.values())[0]
else:
return Error(message="Plugin not found", status=404)
return response

52
senpy/cli.py Normal file
View File

@@ -0,0 +1,52 @@
import sys
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'}
'''
cli_dict = {}
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
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)
return cli_params, cli_dict
def main_function(argv):
'''This is the method for unit testing
'''
cli_params, cli_dict = parse_cli(argv)
plugin_folder = cli_params['plugin_folder']
sp = Senpy(default_plugins=False, plugin_folder=plugin_folder)
sp.activate_all(sync=True)
res = sp.analyse(**cli_dict)
return res
def main():
'''This method is the entrypoint for the CLI (as configured un setup.py)
'''
try:
res = main_function(sys.argv[1:])
print(res.to_JSON())
except Error as err:
print(err.to_JSON())
sys.exit(2)
if __name__ == '__main__':
main()

37
senpy/client.py Normal file
View File

@@ -0,0 +1,37 @@
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)
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 isinstance(resp, models.Error):
raise resp
return resp

View File

@@ -1,38 +0,0 @@
{
"dc": "http://purl.org/dc/terms/",
"dc:subject": {
"@type": "@id"
},
"xsd": "http://www.w3.org/2001/XMLSchema#",
"marl": "http://www.gsi.dit.upm.es/ontologies/marl/ns#",
"nif": "http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#",
"onyx": "http://www.gsi.dit.upm.es/ontologies/onyx/ns#",
"emotions": {
"@id": "onyx:hasEmotionSet",
"@type": "onyx:EmotionSet"
},
"opinions": {
"@container": "@list",
"@id": "marl:hasOpinion",
"@type": "marl:Opinion"
},
"prov": "http://www.w3.org/ns/prov#",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"analysis": {
"@id": "prov:wasInformedBy"
},
"entries": {
"@id": "prov:generated"
},
"strings": {
"@reverse": "nif:hasContext",
"@type": "nif:String"
},
"date":
{
"@id": "dc:date",
"@type": "xsd:dateTime"
},
"wnaffect": "http://www.gsi.dit.upm.es/ontologies/wnaffect#",
"xsd": "http://www.w3.org/2001/XMLSchema#"
}

View File

@@ -1,114 +1,401 @@
"""
Main class for Senpy.
It orchestrates plugin (de)activation and analysis.
"""
from future import standard_library
standard_library.install_aliases()
from . import plugins
from .plugins import SenpyPlugin
from .models import Error, Entry, Results
from .blueprints import api_blueprint, demo_blueprint, ns_blueprint
from .api import API_PARAMS, NIF_PARAMS, parse_params
from threading import Thread
import os
import copy
import fnmatch
import inspect
import sys
import importlib
import logging
import traceback
import yaml
import pip
from flask import current_app
logger = logging.getLogger(__name__)
try:
from flask import _app_ctx_stack as stack
except ImportError:
from flask import _request_ctx_stack as stack
from blueprints import nif_blueprint
from git import Repo, InvalidGitRepositoryError
class Senpy(object):
""" Default Senpy extension for Flask """
def __init__(self, app=None, plugin_folder="plugins"):
def __init__(self,
app=None,
plugin_folder=".",
default_plugins=False):
self.app = app
base_folder = os.path.join(os.path.dirname(__file__), "plugins")
self._search_folders = set()
self._plugin_list = []
self._outdated = True
self._default = None
self.search_folders = (folder for folder in (base_folder, plugin_folder)
if folder and os.path.isdir(folder))
self.add_folder(plugin_folder)
if default_plugins:
self.add_folder('plugins', from_root=True)
else:
# Add only conversion plugins
self.add_folder(os.path.join('plugins', 'conversion'),
from_root=True)
if app is not None:
self.init_app(app)
"""
def init_app(self, app):
""" Initialise a flask app to add plugins to its context """
"""
Note: I'm not particularly fond of adding self.app and app.senpy, but
I can't think of a better way to do it.
"""
def init_app(self, app, plugin_folder="plugins"):
"""
app.senpy = self
#app.config.setdefault('SQLITE3_DATABASE', ':memory:')
# Use the newstyle teardown_appcontext if it's available,
# otherwise fall back to the request context
if hasattr(app, 'teardown_appcontext'):
app.teardown_appcontext(self.teardown)
else:
app.teardown_request(self.teardown)
app.register_blueprint(nif_blueprint)
app.register_blueprint(api_blueprint, url_prefix="/api")
app.register_blueprint(ns_blueprint, url_prefix="/ns")
app.register_blueprint(demo_blueprint, url_prefix="/")
def analyse(self, **params):
if "algorithm" in params:
algo = params["algorithm"]
if algo in self.plugins and self.plugins[algo].enabled:
plug = self.plugins[algo]
resp = plug.analyse(**params)
resp.analysis.append(plug.jsonable())
return resp
return {"status": 500, "message": "No valid algorithm"}
def add_folder(self, folder, from_root=False):
if from_root:
folder = os.path.join(os.path.dirname(__file__), folder)
logger.debug("Adding folder: %s", folder)
if os.path.isdir(folder):
self._search_folders.add(folder)
self._outdated = True
else:
logger.debug("Not a folder: %s", folder)
def parameters(self, algo):
if algo in self.plugins:
if hasattr(self.plugins[algo], "parameters"):
return self.plugins[algo].parameters
return {}
def _find_plugin(self, params):
api_params = parse_params(params, spec=API_PARAMS)
algo = None
if "algorithm" in api_params and api_params["algorithm"]:
algo = api_params["algorithm"]
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))
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))
def _load_plugin(self, plugin, search_folder, enabled=True):
sys.path.append(search_folder)
tmp = importlib.import_module(plugin).plugin
sys.path.remove(search_folder)
tmp.path = search_folder
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))
return self.plugins[algo]
def _get_params(self, params, plugin):
nif_params = parse_params(params, spec=NIF_PARAMS)
extra_params = plugin.get('extra_params', {})
specific_params = parse_params(params, spec=extra_params)
nif_params.update(specific_params)
return nif_params
def _get_entries(self, params):
entry = None
if params['informat'] == 'text':
entry = Entry(text=params['input'])
else:
raise NotImplemented('Only text input format implemented')
yield entry
def analyse(self, **api_params):
logger.debug("analysing with params: {}".format(api_params))
plugin = self._find_plugin(api_params)
nif_params = self._get_params(api_params, plugin)
resp = Results()
if 'with_parameters' in api_params:
resp.parameters = nif_params
try:
repo_path = os.path.join(search_folder, plugin)
tmp.repo = Repo(repo_path)
except InvalidGitRepositoryError:
tmp.repo = None
if not hasattr(tmp, "enabled"):
tmp.enabled = enabled
entries = []
for i in self._get_entries(nif_params):
entries += list(plugin.analyse_entry(i, nif_params))
resp.entries = entries
self.convert_emotions(resp, plugin, nif_params)
resp.analysis.append(plugin.id)
logger.debug("Returning analysis result: {}".format(resp))
except Error as ex:
logger.exception('Error returning analysis result')
resp = ex
except Exception as ex:
logger.exception('Error returning analysis result')
resp = Error(message=str(ex), status=500)
return resp
def _conversion_candidates(self, fromModel, toModel):
candidates = self.filter_plugins(**{'@type': 'emotionConversionPlugin'})
for name, candidate in candidates.items():
for pair in candidate.onyx__doesConversion:
logging.debug(pair)
if pair['onyx:conversionFrom'] == fromModel \
and pair['onyx:conversionTo'] == toModel:
# logging.debug('Found candidate: {}'.format(candidate))
yield candidate
def convert_emotions(self, resp, plugin, params):
"""
Conversion of all emotions in a response.
In addition to converting from one model to another, it has
to include the conversion plugin to the analysis list.
Needless to say, this is far from an elegant solution, but it works.
@todo refactor and clean up
"""
fromModel = plugin.get('onyx:usesEmotionModel', None)
toModel = params.get('emotionModel', None)
output = params.get('conversion', None)
logger.debug('Asked for model: {}'.format(toModel))
logger.debug('Analysis plugin uses model: {}'.format(fromModel))
if not toModel:
return
try:
candidate = next(self._conversion_candidates(fromModel, toModel))
except StopIteration:
e = Error(('No conversion plugin found for: '
'{} -> {}'.format(fromModel, toModel)))
e.original_response = resp
e.parameters = params
raise e
newentries = []
for i in resp.entries:
if output == "full":
newemotions = copy.deepcopy(i.emotions)
else:
newemotions = []
for j in i.emotions:
for k in candidate.convert(j, fromModel, toModel, params):
k.prov__wasGeneratedBy = candidate.id
if output == 'nested':
k.prov__wasDerivedFrom = j
newemotions.append(k)
i.emotions = newemotions
newentries.append(i)
resp.entries = newentries
resp.analysis.append(candidate.id)
@property
def default_plugin(self):
candidate = self._default
if not candidate:
candidates = self.filter_plugins(is_activated=True)
if len(candidates) > 0:
candidate = list(candidates.values())[0]
logger.debug("Default: {}".format(candidate))
return candidate
@default_plugin.setter
def default_plugin(self, value):
if isinstance(value, SenpyPlugin):
self._default = value
else:
self._default = self.plugins[value]
def activate_all(self, sync=False):
ps = []
for plug in self.plugins.keys():
ps.append(self.activate_plugin(plug, sync=sync))
return ps
def deactivate_all(self, sync=False):
ps = []
for plug in self.plugins.keys():
ps.append(self.deactivate_plugin(plug, sync=sync))
return ps
def _set_active_plugin(self, plugin_name, active=True, *args, **kwargs):
''' We're using a variable in the plugin itself to activate/deactive plugins.\
Note that plugins may activate themselves by setting this variable.
'''
self.plugins[plugin_name].is_activated = active
def activate_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)
logger.info("Activating plugin: {}".format(plugin.name))
def act():
success = False
try:
plugin.activate()
msg = "Plugin activated: {}".format(plugin.name)
logger.info(msg)
success = True
self._set_active_plugin(plugin_name, success)
except Exception as ex:
msg = "Error activating plugin {} - {} : \n\t{}".format(
plugin.name, ex, traceback.format_exc())
logger.error(msg)
raise Error(msg)
if sync or 'async' in plugin and not plugin.async:
act()
else:
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)
self._set_active_plugin(plugin_name, False)
def deact():
try:
plugin.deactivate()
logger.info("Plugin deactivated: {}".format(plugin.name))
except Exception as ex:
logger.error(
"Error deactivating plugin {}: {}".format(plugin.name, ex))
logger.error("Trace: {}".format(traceback.format_exc()))
if sync or 'async' in plugin and not plugin.async:
deact()
else:
th = Thread(target=deact)
th.start()
@classmethod
def validate_info(cls, info):
return all(x in info for x in ('name', 'module', 'description', 'version'))
def install_deps(self):
for i in self.plugins.values():
self._install_deps(i)
@classmethod
def _install_deps(cls, info=None):
requirements = info.get('requirements', [])
if requirements:
pip_args = []
pip_args.append('install')
pip_args.append('--use-wheel')
for req in requirements:
pip_args.append(req)
logger.info('Installing requirements: ' + str(requirements))
pip.main(pip_args)
@classmethod
def _load_module(cls, name, root):
sys.path.append(root)
tmp = importlib.import_module(name)
sys.path.remove(root)
return tmp
@classmethod
def _load_plugin_from_info(cls, info, root):
if not cls.validate_info(info):
logger.warn('The module info is not valid.\n\t{}'.format(info))
return None, None
module = info["module"]
name = info["name"]
cls._install_deps(info)
tmp = cls._load_module(module, root)
candidate = None
for _, obj in inspect.getmembers(tmp):
if inspect.isclass(obj) and inspect.getmodule(obj) == tmp:
logger.debug(("Found plugin class:"
" {}@{}").format(obj, inspect.getmodule(obj)))
candidate = obj
break
if not candidate:
logger.debug("No valid plugin for: {}".format(module))
return
module = candidate(info=info)
return name, module
@classmethod
def _load_plugin(cls, root, filename):
fpath = os.path.join(root, filename)
logger.debug("Loading plugin: {}".format(fpath))
with open(fpath, 'r') as f:
info = yaml.load(f)
logger.debug("Info: {}".format(info))
return cls._load_plugin_from_info(info, root)
def _load_plugins(self):
#print(sys.path)
#print(search_folder)
plugins = {}
for search_folder in self.search_folders:
for item in os.listdir(search_folder):
if os.path.isdir(os.path.join(search_folder, item)) \
and os.path.exists(
os.path.join(search_folder, item, "__init__.py")):
plugins[item] = self._load_plugin(item, search_folder)
for search_folder in self._search_folders:
for root, dirnames, filenames in os.walk(search_folder):
for filename in fnmatch.filter(filenames, '*.senpy'):
name, plugin = self._load_plugin(root, filename)
if plugin and name:
plugins[name] = plugin
self._outdated = False
return plugins
def teardown(self, exception):
pass
def enable_all(self):
for plugin in self.plugins:
self.enable_plugin(plugin)
def enable_plugin(self, item):
self.plugins[item].enabled = True
def disable_plugin(self, item):
self.plugins[item].enabled = False
@property
def plugins(self):
ctx = stack.top
if ctx is not None:
if not hasattr(self, '_plugins'):
self._plugins = self._load_plugins()
return self._plugins
""" Return the plugins registered for a given application. """
if self._outdated:
self._plugin_list = self._load_plugins()
return self._plugin_list
if __name__ == '__main__':
from flask import Flask
app = Flask(__name__)
sp = Senpy()
sp.init_app(app)
with app.app_context():
sp._load_plugins()
def filter_plugins(self, **kwargs):
""" Filter plugins by different criteria """
ptype = kwargs.pop('plugin_type', None)
logger.debug('#' * 100)
logger.debug('ptype {}'.format(ptype))
if ptype:
try:
ptype = ptype[0].upper() + ptype[1:]
pclass = getattr(plugins, ptype)
logger.debug('Class: {}'.format(pclass))
candidates = filter(lambda x: isinstance(x, pclass),
self.plugins.values())
except AttributeError:
raise Error('{} is not a valid type'.format(ptype))
else:
candidates = self.plugins.values()
logger.debug(candidates)
def matches(plug):
res = all(getattr(plug, k, None) == v for (k, v) in kwargs.items())
logger.debug(
"matching {} with {}: {}".format(plug.name, kwargs, res))
return res
if kwargs:
candidates = filter(matches, candidates)
return {p.name: p for p in candidates}
@property
def analysis_plugins(self):
""" Return only the analysis plugins """
return self.filter_plugins(plugin_type='analysisPlugin')

View File

@@ -1,59 +1,341 @@
'''
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.
'''
from __future__ import print_function
from six import string_types
import time
import copy
import json
import os
from collections import defaultdict
import jsonref
import jsonschema
class Leaf(defaultdict):
def __init__(self, ofclass=list):
super(Leaf, self).__init__(ofclass)
from flask import Response as FlaskResponse
from pyld import jsonld
def __getattr__(self, name):
return super(Leaf, self).__getitem__(name)
from rdflib import Graph
def __setattr__(self, name, value):
self[name] = value
import logging
def __delattr__(self, name):
return super(Leaf, self).__delitem__(name)
logger = logging.getLogger(__name__)
class Response(Leaf):
def __init__(self, context=None):
super(Response, self).__init__()
self["analysis"] = []
self["entries"] = []
if context is None:
context = "{}/context.jsonld".format(os.path.dirname(
os.path.realpath(__file__)))
if isinstance(context, dict):
self["@context"] = context
if isinstance(context, basestring):
DEFINITIONS_FILE = 'definitions.json'
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)
def read_schema(schema_file, absolute=False):
schema_path = get_schema_path(schema_file, absolute)
schema_uri = 'file://{}'.format(schema_path)
with open(schema_path) as f:
return jsonref.load(f, base_uri=schema_uri)
base_schema = read_schema(DEFINITIONS_FILE)
class Context(dict):
@staticmethod
def load(context):
logging.debug('Loading context: {}'.format(context))
if not context:
return context
elif isinstance(context, list):
contexts = []
for c in context:
contexts.append(Context.load(c))
return contexts
elif isinstance(context, dict):
return Context(context)
elif isinstance(context, string_types):
try:
with open(context) as f:
self["@context"] = json.loads(f.read())
return Context(json.loads(f.read()))
except IOError:
self["@context"] = context
return context
else:
raise AttributeError('Please, provide a valid context')
class Entry(Leaf):
def __init__(self, text=None, emotionSets=None, opinions=None, **kwargs):
super(Entry, self).__init__(**kwargs)
if text:
self.text = text
if emotionSets:
self.emotionSets = emotionSets
if opinions:
self.opinions = opinions
class Opinion(Leaf):
def __init__(self, polarityValue=None, polarity=None, **kwargs):
super(Opinion, self).__init__(**kwargs)
if polarityValue is not None:
self.polarityValue = polarityValue
if polarity is not None:
self.polarity = polarity
base_context = Context.load(CONTEXT_PATH)
class SenpyMixin(object):
_context = base_context["@context"]
class EmotionSet(Leaf):
def __init__(self, emotions=[], **kwargs):
super(EmotionSet, self).__init__(**kwargs)
self.emotions = emotions or []
def flask(self,
in_headers=True,
headers=None,
outformat='json-ld',
**kwargs):
"""
Return the values and error to be used in flask.
So far, it returns a fixed context. We should store/generate different
contexts if the plugin adds more aliases.
"""
headers = headers or {}
kwargs["with_context"] = not in_headers
content, mimetype = self.serialize(format=outformat,
with_mime=True,
**kwargs)
if outformat == 'json-ld' and in_headers:
headers.update({
"Link":
('<%s>;'
'rel="http://www.w3.org/ns/json-ld#context";'
' type="application/ld+json"' % kwargs.get('context_uri'))
})
return FlaskResponse(
response=content,
status=getattr(self, "status", 200),
headers=headers,
mimetype=mimetype)
def serialize(self, format='json-ld', with_mime=False, **kwargs):
js = self.jsonld(**kwargs)
if format == 'json-ld':
content = json.dumps(js, indent=2, sort_keys=True)
mimetype = "application/json"
elif format in ['turtle', ]:
logger.debug(js)
content = json.dumps(js, indent=2, sort_keys=True)
g = Graph().parse(
data=content,
format='json-ld',
base=kwargs.get('prefix'),
context=self._context)
logger.debug(
'Parsing with prefix: {}'.format(kwargs.get('prefix')))
content = g.serialize(format='turtle').decode('utf-8')
mimetype = 'text/{}'.format(format)
else:
raise Error('Unknown outformat: {}'.format(format))
if with_mime:
return content, mimetype
else:
return content
def serializable(self):
def ser_or_down(item):
if hasattr(item, 'serializable'):
return item.serializable()
elif isinstance(item, dict):
temp = dict()
for kp in item:
vp = item[kp]
temp[kp] = ser_or_down(vp)
return temp
elif isinstance(item, list):
return list(ser_or_down(i) for i in item)
else:
return item
return ser_or_down(self._plain_dict())
def jsonld(self,
with_context=True,
context_uri=None,
prefix=None,
expanded=False):
ser = self.serializable()
result = jsonld.compact(
ser,
self._context,
options={
'base': prefix,
'expandContext': self._context,
'senpy': prefix
})
if context_uri:
result['@context'] = context_uri
if expanded:
result = jsonld.expand(
result, options={'base': prefix,
'expandContext': self._context})
if not with_context:
del result['@context']
return result
def to_JSON(self, *args, **kwargs):
js = json.dumps(self.jsonld(*args, **kwargs), indent=4, sort_keys=True)
return js
def validate(self, obj=None):
if not obj:
obj = self
if hasattr(obj, "jsonld"):
obj = obj.jsonld()
jsonschema.validate(obj, self.schema)
def __str__(self):
return str(self.to_JSON())
class BaseModel(SenpyMixin, dict):
schema = base_schema
def __init__(self, *args, **kwargs):
if 'id' in kwargs:
self.id = kwargs.pop('id')
elif kwargs.pop('_auto_id', True):
self.id = '_:{}_{}'.format(type(self).__name__, time.time())
temp = dict(*args, **kwargs)
for obj in [
self.schema,
] + self.schema.get('allOf', []):
for k, v in obj.get('properties', {}).items():
if 'default' in v and k not in temp:
temp[k] = copy.deepcopy(v['default'])
for i in temp:
nk = self._get_key(i)
if nk != i:
temp[nk] = temp[i]
del temp[i]
try:
temp['@type'] = getattr(self, '@type')
except AttributeError:
logger.warn('Creating an instance of an unknown model')
super(BaseModel, self).__init__(temp)
def _get_key(self, key):
key = key.replace("__", ":", 1)
return key
def __setitem__(self, key, value):
dict.__setitem__(self, key, value)
def __delitem__(self, key):
dict.__delitem__(self, key)
def __getattr__(self, key):
try:
return self.__getitem__(self._get_key(key))
except KeyError:
raise AttributeError(key)
def __setattr__(self, key, value):
self.__setitem__(self._get_key(key), value)
def __delattr__(self, key):
self.__delitem__(self._get_key(key))
def _plain_dict(self):
d = {k: v for (k, v) in self.items() if k[0] != "_"}
if 'id' in d:
d["@id"] = d.pop('id')
return d
_subtypes = {}
def register(rsubclass, rtype=None):
_subtypes[rtype or rsubclass.__name__] = rsubclass
def from_dict(indict):
target = indict.get('@type', None)
if target and target in _subtypes:
cls = _subtypes[target]
else:
cls = BaseModel
return cls(**indict)
def from_json(injson):
indict = json.loads(injson)
return from_dict(indict)
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(name[0].upper(), name[1:])
newclass = type(class_name, tuple(base_classes), {})
setattr(newclass, '@type', name)
setattr(newclass, 'schema', read_schema(schema_file))
setattr(newclass, 'class_name', class_name)
register(newclass, name)
return newclass
def _add_from_schema(*args, **kwargs):
generatedClass = from_schema(*args, **kwargs)
globals()[generatedClass.__name__] = generatedClass
del generatedClass
for i in [
'analysis',
'emotion',
'emotionConversion',
'emotionConversionPlugin',
'emotionAnalysis',
'emotionModel',
'emotionPlugin',
'emotionSet',
'entry',
'plugin',
'plugins',
'response',
'results',
'sentiment',
'sentimentPlugin',
'suggestion',
]:
_add_from_schema(i)
_ErrorModel = from_schema('error')
class Error(SenpyMixin, BaseException):
def __init__(self, message, *args, **kwargs):
super(Error, self).__init__(self, message, message)
self._error = _ErrorModel(message=message, *args, **kwargs)
self.message = message
def __getitem__(self, key):
return self._error[key]
def __setitem__(self, key, value):
self._error[key] = value
def __delitem__(self, key):
del self._error[key]
def __getattr__(self, key):
if key != '_error' and hasattr(self._error, key):
return getattr(self._error, key)
raise AttributeError(key)
def __setattr__(self, key, value):
if key != '_error':
return setattr(self._error, key, value)
else:
super(Error, self).__setattr__(key, value)
def __delattr__(self, key):
delattr(self._error, key)
register(Error, 'error')

View File

@@ -1,11 +1,34 @@
class SenpyPlugin(object):
def __init__(self, name=None, version=None, params=None):
self.name = name
self.version = version
self.params = params or []
from future import standard_library
standard_library.install_aliases()
def analyse(self, *args, **kwargs):
pass
import inspect
import os.path
import os
import pickle
import logging
import tempfile
import copy
from .. import models
logger = logging.getLogger(__name__)
class SenpyPlugin(models.Plugin):
def __init__(self, info=None):
"""
Provides a canonical name for plugins and serves as base for other
kinds of plugins.
"""
if not info:
raise models.Error(message=("You need to provide configuration"
"information for the plugin."))
logger.debug("Initialising {}".format(info))
id = 'plugins/{}_{}'.format(info['name'], info['version'])
super(SenpyPlugin, self).__init__(id=id, **info)
self.is_activated = False
def get_folder(self):
return os.path.dirname(inspect.getfile(self.__class__))
def activate(self):
pass
@@ -13,42 +36,75 @@ class SenpyPlugin(object):
def deactivate(self):
pass
def jsonable(self, parameters=False):
resp = {
"@id": "{}_{}".format(self.name, self.version),
}
if parameters:
resp["parameters"] = self.params,
return resp
class SentimentPlugin(SenpyPlugin):
def __init__(self,
minPolarityValue=0,
maxPolarityValue=1,
**kwargs):
super(SentimentPlugin, self).__init__(**kwargs)
self.minPolarityValue = minPolarityValue
self.maxPolarityValue = maxPolarityValue
class AnalysisPlugin(SenpyPlugin):
def jsonable(self, *args, **kwargs):
resp = super(SentimentPlugin, self).jsonable(*args, **kwargs)
resp["marl:maxPolarityValue"] = self.maxPolarityValue
resp["marl:minPolarityValue"] = self.minPolarityValue
return resp
def analyse(self, *args, **kwargs):
raise NotImplemented(
'Your method should implement either analyse or analyse_entry')
class EmotionPlugin(SenpyPlugin):
def __init__(self,
minEmotionValue=0,
maxEmotionValue=1,
emotionCategory=None,
**kwargs):
super(EmotionPlugin, self).__init__(**kwargs)
self.minEmotionValue = minEmotionValue
self.maxEmotionValue = maxEmotionValue
self.emotionCategory = emotionCategory
def analyse_entry(self, entry, parameters):
""" An implemented plugin should override this method.
This base method is here to adapt old style plugins which only
implement the *analyse* function.
Note that this method may yield an annotated entry or a list of
entries (e.g. in a tokenizer)
"""
text = entry['text']
params = copy.copy(parameters)
params['input'] = text
results = self.analyse(**params)
for i in results.entries:
yield i
def jsonable(self, *args, **kwargs):
resp = super(EmotionPlugin, self).jsonable(*args, **kwargs)
resp["onyx:minEmotionValue"] = self.minEmotionValue
resp["onyx:maxEmotionValue"] = self.maxEmotionValue
return resp
class ConversionPlugin(SenpyPlugin):
pass
class SentimentPlugin(models.SentimentPlugin, AnalysisPlugin):
def __init__(self, info, *args, **kwargs):
super(SentimentPlugin, self).__init__(info, *args, **kwargs)
self.minPolarityValue = float(info.get("minPolarityValue", 0))
self.maxPolarityValue = float(info.get("maxPolarityValue", 1))
class EmotionPlugin(models.EmotionPlugin, AnalysisPlugin):
def __init__(self, info, *args, **kwargs):
super(EmotionPlugin, self).__init__(info, *args, **kwargs)
self.minEmotionValue = float(info.get("minEmotionValue", -1))
self.maxEmotionValue = float(info.get("maxEmotionValue", 1))
class EmotionConversionPlugin(models.EmotionConversionPlugin, ConversionPlugin):
pass
class ShelfMixin(object):
@property
def sh(self):
if not hasattr(self, '_sh') or self._sh is None:
self.__dict__['_sh'] = {}
if os.path.isfile(self.shelf_file):
self.__dict__['_sh'] = pickle.load(open(self.shelf_file, 'rb'))
return self._sh
@sh.deleter
def sh(self):
if os.path.isfile(self.shelf_file):
os.remove(self.shelf_file)
del self.__dict__['_sh']
self.save()
@property
def shelf_file(self):
if 'shelf_file' not in self or not self['shelf_file']:
sd = os.environ.get('SENPY_DATA', tempfile.gettempdir())
self.shelf_file = os.path.join(sd, self.name + '.p')
return self['shelf_file']
def save(self):
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)

View File

View File

@@ -0,0 +1,52 @@
from senpy.plugins import EmotionConversionPlugin
from senpy.models import EmotionSet, Emotion, Error
import logging
logger = logging.getLogger(__name__)
class CentroidConversion(EmotionConversionPlugin):
def _forward_conversion(self, original):
"""Sum the VAD value of all categories found."""
res = Emotion()
for e in original.onyx__hasEmotion:
category = e.onyx__hasEmotionCategory
if category in self.centroids:
for dim, value in self.centroids[category].items():
try:
res[dim] += value
except Exception:
res[dim] = value
return res
def _backwards_conversion(self, original):
"""Find the closest category"""
dimensions = list(self.centroids.values())[0]
def distance(e1, e2):
return sum((e1[k] - e2.get(self.aliases[k], 0)) for k in dimensions)
emotion = ''
mindistance = 10000000000000000000000.0
for state in self.centroids:
d = distance(self.centroids[state], original)
if d < mindistance:
mindistance = d
emotion = state
result = Emotion(onyx__hasEmotionCategory=emotion)
return result
def convert(self, emotionSet, fromModel, toModel, params):
cf, ct = self.centroids_direction
logger.debug('{}\n{}\n{}\n{}'.format(emotionSet, fromModel, toModel, params))
e = EmotionSet()
if fromModel == cf:
e.onyx__hasEmotion.append(self._forward_conversion(emotionSet))
elif fromModel == ct:
for i in emotionSet.onyx__hasEmotion:
e.onyx__hasEmotion.append(self._backwards_conversion(i))
else:
raise Error('EMOTION MODEL NOT KNOWN')
yield e

View File

@@ -0,0 +1,38 @@
---
name: Ekman2VAD
module: senpy.plugins.conversion.centroids
description: Plugin to convert emotion sets from Ekman to VAD
version: 0.1
onyx:doesConversion:
- onyx:conversionFrom: emoml:big6
onyx:conversionTo: emoml:fsre-dimensions
- onyx:conversionFrom: emoml:fsre-dimensions
onyx:conversionTo: emoml:big6
centroids:
emoml:big6anger:
A: 6.95
D: 5.1
V: 2.7
emoml:big6disgust:
A: 5.3
D: 8.05
V: 2.7
emoml:big6fear:
A: 6.5
D: 3.6
V: 3.2
emoml:big6happiness:
A: 7.22
D: 6.28
V: 8.6
emoml:big6sadness:
A: 5.21
D: 2.82
V: 2.21
centroids_direction:
- emoml:big6
- emoml:fsre-dimensions
aliases:
A: emoml:arousal
V: emoml:valence
D: emoml:dominance

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
---
name: sentiment140
module: sentiment140
description: "Connects to the sentiment140 free API: http://sentiment140.com"
author: "@balkian"
version: '0.2'
url: "https://github.com/gsi-upm/senpy-plugins-community"
extra_params:
language:
"@id": lang_sentiment140
aliases:
- language
- l
required: false
options:
- es
- en
- auto
requirements: {}
maxPolarityValue: 1
minPolarityValue: 0

View File

@@ -1,48 +0,0 @@
import requests
import json
import sys
print(sys.path)
from senpy.plugins import SentimentPlugin
from senpy.models import Response, Opinion, Entry
class Sentiment140Plugin(SentimentPlugin):
parameters = {
"language": {"aliases": ["language", "l"],
"required": False,
"options": ["es", "en", "auto"],
}
}
def __init__(self, **kwargs):
super(Sentiment140Plugin, self).__init__(name="Sentiment140",
version="1.0",
**kwargs)
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"]}]}
))
response = Response()
polarityValue = int(res.json()["data"][0]["polarity"]) * 25
polarity = "marl:Neutral"
if polarityValue > 50:
polarity = "marl:Positive"
elif polarityValue < 50:
polarity = "marl:Negative"
entry = Entry(text=params["input"])
opinion = Opinion(polarity=polarity, polarityValue=polarityValue)
entry.opinions.append(opinion)
entry.language = lang
response.entries.append(entry)
return response
plugin = Sentiment140Plugin()

View File

@@ -1,41 +0,0 @@
'''
SENTIMENT140
=============
* http://www.sentiment140.com/api/bulkClassifyJson
* Method: POST
* Parameters: JSON Object (that is copied to the result)
* text
* query
* language
* topic
* Example response:
```json
{"data": [{"text": "I love Titanic.", "id":1234, "polarity": 4},
{"text": "I hate Titanic.", "id":4567, "polarity": 0}]}
```
'''
import requests
import json
ENDPOINT_URI = "http://www.sentiment140.com/api/bulkClassifyJson"
def analyse(texts):
parameters = {"data": []}
if isinstance(texts, list):
for text in texts:
parameters["data"].append({"text": text})
else:
parameters["data"].append({"text": texts})
res = requests.post(ENDPOINT_URI, json.dumps(parameters))
res.json()
return res.json()
def test():
print analyse("I love Titanic")
print analyse(["I love Titanic", "I hate Titanic"])
if __name__ == "__main__":
test()

View File

@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"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

@@ -0,0 +1,56 @@
{
"@context": {
"@vocab": "http://www.gsi.dit.upm.es/ontologies/senpy#",
"dc": "http://dublincore.org/2012/06/14/dcelements#",
"me": "http://www.mixedemotions-project.eu/ns/model#",
"prov": "http://www.w3.org/ns/prov#",
"nif": "http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#",
"marl": "http://www.gsi.dit.upm.es/ontologies/marl/ns#",
"onyx": "http://www.gsi.dit.upm.es/ontologies/onyx/ns#",
"wna": "http://www.gsi.dit.upm.es/ontologies/wnaffect/ns#",
"emoml": "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/emotionml/ns#",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"topics": {
"@id": "dc:subject"
},
"entities": {
"@id": "me:hasEntities"
},
"suggestions": {
"@id": "me:hasSuggestions",
"@container": "@set"
},
"emotions": {
"@id": "onyx:hasEmotionSet",
"@container": "@set"
},
"sentiments": {
"@id": "marl:hasOpinion",
"@container": "@set"
},
"entries": {
"@id": "prov:used",
"@container": "@set"
},
"analysis": {
"@id": "AnalysisInvolved",
"@type": "@id",
"@container": "@set"
},
"prov:wasGeneratedBy": {
"@type": "@id"
},
"onyx:usesEmotionModel": {
"@type": "@id"
},
"onyx:hasEmotionCategory": {
"@type": "@id"
},
"onyx:conversionFrom": {
"@type": "@id"
},
"onyx:conversionTo": {
"@type": "@id"
}
}
}

View File

@@ -0,0 +1,45 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"Results": {
"$ref": "results.json"
},
"Context": {
"$ref": "context.json"
},
"Analysis": {
"$ref": "analysis.json"
},
"Entry": {
"$ref": "entry.json"
},
"Sentiment": {
"$ref": "sentiment.json"
},
"EmotionSet": {
"$ref": "emotionSet.json"
},
"Emotion": {
"$ref": "emotion.json"
},
"EmotionModel": {
"$ref": "emotionModel.json"
},
"Entity": {
"$ref": "entity.json"
},
"Topic": {
"$ref": "topic.json"
},
"Suggestion": {
"$ref": "suggestion.json"
},
"Plugins": {
"$ref": "plugin.json"
},
"Plugin": {
"$ref": "plugin.json"
},
"Response": {
"$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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
{
"$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: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"
}

39
senpy/schemas/entry.json Normal file
View File

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

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

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

19
senpy/schemas/plugin.json Normal file
View File

@@ -0,0 +1,19 @@
{
"$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

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

View File

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

View File

@@ -0,0 +1,39 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"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

@@ -0,0 +1,23 @@
{
"$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"
},
"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

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

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

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

347
senpy/static/css/bootstrap-theme.css vendored Normal file
View File

@@ -0,0 +1,347 @@
/*!
* Bootstrap v3.1.1 (http://getbootstrap.com)
* Copyright 2011-2014 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
.btn-default,
.btn-primary,
.btn-success,
.btn-info,
.btn-warning,
.btn-danger {
text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
}
.btn-default:active,
.btn-primary:active,
.btn-success:active,
.btn-info:active,
.btn-warning:active,
.btn-danger:active,
.btn-default.active,
.btn-primary.active,
.btn-success.active,
.btn-info.active,
.btn-warning.active,
.btn-danger.active {
-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
}
.btn:active,
.btn.active {
background-image: none;
}
.btn-default {
text-shadow: 0 1px 0 #fff;
background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #dbdbdb;
border-color: #ccc;
}
.btn-default:hover,
.btn-default:focus {
background-color: #e0e0e0;
background-position: 0 -15px;
}
.btn-default:active,
.btn-default.active {
background-color: #e0e0e0;
border-color: #dbdbdb;
}
.btn-primary {
background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #2b669a;
}
.btn-primary:hover,
.btn-primary:focus {
background-color: #2d6ca2;
background-position: 0 -15px;
}
.btn-primary:active,
.btn-primary.active {
background-color: #2d6ca2;
border-color: #2b669a;
}
.btn-success {
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #3e8f3e;
}
.btn-success:hover,
.btn-success:focus {
background-color: #419641;
background-position: 0 -15px;
}
.btn-success:active,
.btn-success.active {
background-color: #419641;
border-color: #3e8f3e;
}
.btn-info {
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #28a4c9;
}
.btn-info:hover,
.btn-info:focus {
background-color: #2aabd2;
background-position: 0 -15px;
}
.btn-info:active,
.btn-info.active {
background-color: #2aabd2;
border-color: #28a4c9;
}
.btn-warning {
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #e38d13;
}
.btn-warning:hover,
.btn-warning:focus {
background-color: #eb9316;
background-position: 0 -15px;
}
.btn-warning:active,
.btn-warning.active {
background-color: #eb9316;
border-color: #e38d13;
}
.btn-danger {
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #b92c28;
}
.btn-danger:hover,
.btn-danger:focus {
background-color: #c12e2a;
background-position: 0 -15px;
}
.btn-danger:active,
.btn-danger.active {
background-color: #c12e2a;
border-color: #b92c28;
}
.thumbnail,
.img-thumbnail {
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
}
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
background-color: #e8e8e8;
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
background-repeat: repeat-x;
}
.dropdown-menu > .active > a,
.dropdown-menu > .active > a:hover,
.dropdown-menu > .active > a:focus {
background-color: #357ebd;
background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
background-repeat: repeat-x;
}
.navbar-default {
background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
}
.navbar-default .navbar-nav > .active > a {
background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%);
background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);
background-repeat: repeat-x;
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
}
.navbar-brand,
.navbar-nav > li > a {
text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
}
.navbar-inverse {
background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
}
.navbar-inverse .navbar-nav > .active > a {
background-image: -webkit-linear-gradient(top, #222 0%, #282828 100%);
background-image: linear-gradient(to bottom, #222 0%, #282828 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);
background-repeat: repeat-x;
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
}
.navbar-inverse .navbar-brand,
.navbar-inverse .navbar-nav > li > a {
text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
}
.navbar-static-top,
.navbar-fixed-top,
.navbar-fixed-bottom {
border-radius: 0;
}
.alert {
text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
}
.alert-success {
background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
background-repeat: repeat-x;
border-color: #b2dba1;
}
.alert-info {
background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
background-repeat: repeat-x;
border-color: #9acfea;
}
.alert-warning {
background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
background-repeat: repeat-x;
border-color: #f5e79e;
}
.alert-danger {
background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
background-repeat: repeat-x;
border-color: #dca7a7;
}
.progress {
background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar {
background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-success {
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-info {
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-warning {
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-danger {
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
background-repeat: repeat-x;
}
.list-group {
border-radius: 4px;
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
}
.list-group-item.active,
.list-group-item.active:hover,
.list-group-item.active:focus {
text-shadow: 0 -1px 0 #3071a9;
background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);
background-repeat: repeat-x;
border-color: #3278b3;
}
.panel {
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
}
.panel-default > .panel-heading {
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
background-repeat: repeat-x;
}
.panel-primary > .panel-heading {
background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
background-repeat: repeat-x;
}
.panel-success > .panel-heading {
background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
background-repeat: repeat-x;
}
.panel-info > .panel-heading {
background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
background-repeat: repeat-x;
}
.panel-warning > .panel-heading {
background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
background-repeat: repeat-x;
}
.panel-danger > .panel-heading {
background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
background-repeat: repeat-x;
}
.well {
background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
background-repeat: repeat-x;
border-color: #dcdcdc;
-webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
}
/*# sourceMappingURL=bootstrap-theme.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5785
senpy/static/css/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

7
senpy/static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

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

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