Compare commits
118 Commits
0.2.2
...
0.7.1-5-g8
Author | SHA1 | Date | |
---|---|---|---|
|
87589e994a | ||
|
9f6a6f5ecd | ||
|
3cea7534ef | ||
|
7eaf303124 | ||
|
b4ca5f4a7c | ||
|
3311af2167 | ||
|
a4694dff2c | ||
|
6cb669cdb1 | ||
|
506feec13d | ||
|
2e3a6b7c84 | ||
|
7cc8b562f4 | ||
|
528bbcac35 | ||
|
068241fb72 | ||
|
39d86a2050 | ||
|
5371c83ab0 | ||
|
673992dbe8 | ||
|
eb3a42c247 | ||
|
20357d2a0d | ||
|
e9d7980e42 | ||
|
908090f634 | ||
|
cb963dc438 | ||
|
477cb18db1 | ||
|
fbf0384985 | ||
|
7a2c016cc6 | ||
|
b072121e20 | ||
|
ceed9b97d0 | ||
|
2dbdb58b06 | ||
|
db30257373 | ||
|
7fd69cc690 | ||
|
b543a4614e | ||
|
bc1f9e4cf5 | ||
|
d72a995fa9 | ||
|
40b67503ce | ||
|
8624562f02 | ||
|
4dee623ef9 | ||
|
2e7530d9bc | ||
|
07b5dd3823 | ||
|
0d511ad3c3 | ||
|
7205a0e7b2 | ||
|
fff38bf825 | ||
|
5d5de0bc50 | ||
|
0454fb1afe | ||
|
5e36c71fa7 | ||
|
c8e742f96e | ||
|
1e7ae13700 | ||
|
bf30c04a52 | ||
|
16ce767d09 | ||
|
39761e0922 | ||
|
03eb38c12d | ||
|
a50f026701 | ||
|
b8339e397b | ||
|
407d17b2b9 | ||
|
56fef9e835 | ||
|
14a3e4103b | ||
|
48d7d1d02e | ||
|
14c9f61864 | ||
|
a79df7a3da | ||
|
ecc2a8312a | ||
|
aafd6a0938 | ||
|
b88d6c53e0 | ||
|
4f2aee5681 | ||
|
a5c27bcaba | ||
|
cefd6331e0 | ||
|
c2bb93e86c | ||
|
091104bc7d | ||
|
81cbe5ea54 | ||
|
ab2c1f73e4 | ||
|
6a84af1c5a | ||
|
5983493b78 | ||
|
61deabe13e | ||
|
bb1b4d3266 | ||
|
703fb68b27 | ||
|
6fe68e3c40 | ||
|
7b9f8a8bef | ||
|
82496dc8e4 | ||
|
f74ee668b6 | ||
|
d304dec2f7 | ||
|
45838e7e98 | ||
|
ff002c818a | ||
|
79d6b6f67f | ||
|
b8993f7d64 | ||
|
bd2e0f0d5c | ||
|
7de5b41340 | ||
|
a63e9209fd | ||
|
b0eb2e0628 | ||
|
60415f8217 | ||
|
724eac38d8 | ||
|
8fa372de15 | ||
|
a1ffe04a30 | ||
|
74b0cf868e | ||
|
50e8e2730b | ||
|
b484b453e0 | ||
|
7c2e0ddec7 | ||
|
384aba4654 | ||
|
a857dd3042 | ||
|
b1b672f66d | ||
|
09d9143a82 | ||
|
c1a6b57ac5 | ||
|
6b78b7ccc7 | ||
|
f0b1cfcba6 | ||
|
4bcd046016 | ||
|
ae09f609c2 | ||
|
d1006bbc92 | ||
|
d58137e8f9 | ||
|
79c83e34a3 | ||
|
37a098109f | ||
|
ff14925056 | ||
|
10f4782ad7 | ||
|
4351f76b60 | ||
|
86f45f8147 | ||
|
2834967026 | ||
|
2f7a8d7267 | ||
|
2b68838514 | ||
|
eaf65f0c6b | ||
|
a5e79bead3 | ||
|
54492472ba | ||
|
ff8d12074b | ||
|
bdf1992775 |
9
.drone.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
build:
|
||||
image: python:$$PYTHON_VERSION
|
||||
commands:
|
||||
- python setup.py test
|
||||
|
||||
matrix:
|
||||
PYTHON_VERSION:
|
||||
- 2.7
|
||||
- 3.4
|
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
*.pyc
|
||||
.*
|
||||
*egg-info
|
||||
dist
|
||||
build
|
||||
README.html
|
||||
__pycache__
|
||||
VERSION
|
80
.gitlab-ci.yml
Normal file
@@ -0,0 +1,80 @@
|
||||
image: docker:latest
|
||||
|
||||
|
||||
# When using dind, it's wise to use the overlayfs driver for
|
||||
# improved performance.
|
||||
variables:
|
||||
DOCKER_DRIVER: overlay
|
||||
DOCKERFILE: Dockerfile
|
||||
|
||||
before_script:
|
||||
- sh version.sh > senpy/VERSION
|
||||
|
||||
stages:
|
||||
- test
|
||||
- images
|
||||
- release
|
||||
|
||||
.test: &test_definition
|
||||
variables:
|
||||
PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache"
|
||||
cache:
|
||||
paths:
|
||||
- .eggs/
|
||||
- "$CI_PROJECT_DIR/pip-cache"
|
||||
- .venv
|
||||
key: "$CI_PROJECT_NAME"
|
||||
stage: test
|
||||
script:
|
||||
- pip install --use-wheel -U pip setuptools virtualenv
|
||||
- virtualenv .venv/$PYTHON_VERSION
|
||||
- source .venv/$PYTHON_VERSION/bin/activate
|
||||
- pip install --use-wheel -r requirements.txt
|
||||
- pip install --use-wheel -r test-requirements.txt
|
||||
- py.test --cov=senpy --cov-report term-missing
|
||||
- python
|
||||
|
||||
test-3.5:
|
||||
<<: *test_definition
|
||||
image: "python:3.5"
|
||||
|
||||
test-2.7:
|
||||
<<: *test_definition
|
||||
image: "python:2.7"
|
||||
|
||||
|
||||
.image: &image_definition
|
||||
variables:
|
||||
PYTHON_VERSION: "3.5"
|
||||
before_script:
|
||||
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
|
||||
script:
|
||||
- docker build -f Dockerfile-3.5 . -t $CI_REGISTRY_IMAGE:$CI_BUILD_REF_SLUG-$PYTHON_VERSION
|
||||
- docker push $CI_REGISTRY_IMAGE:$CI_BUILD_REF_SLUG-$PYTHON_VERSION
|
||||
stage: images
|
||||
only:
|
||||
- tags
|
||||
- triggers
|
||||
|
||||
image-3.5:
|
||||
<<: *image_definition
|
||||
variables:
|
||||
PYTHON_VERSION: "3.5"
|
||||
|
||||
image-2.7:
|
||||
<<: *image_definition
|
||||
variables:
|
||||
PYTHON_VERSION: "2.7"
|
||||
|
||||
image-latest:
|
||||
stage: release
|
||||
before_script:
|
||||
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
|
||||
script:
|
||||
- docker build -f Dockerfile . -t $CI_REGISTRY_IMAGE
|
||||
- docker tag $CI_REGISTRY_IMAGE $CI_REGISTRY_IMAGE:$CI_BUILD_REF_SLUG
|
||||
- docker push $CI_REGISTRY_IMAGE
|
||||
- docker push $CI_REGISTRY_IMAGE:$CI_BUILD_REF_SLUG
|
||||
only:
|
||||
- master
|
||||
- triggers
|
8
.travis.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
install: "pip install -r requirements.txt"
|
||||
# run nosetests - Tests
|
||||
script: nosetests
|
1
Dockerfile
Symbolic link
@@ -0,0 +1 @@
|
||||
Dockerfile-3.5
|
21
Dockerfile-2.7
Normal file
@@ -0,0 +1,21 @@
|
||||
from python:2.7
|
||||
|
||||
RUN mkdir /cache/
|
||||
ENV PIP_CACHE_DIR=/cache/
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ADD requirements.txt /usr/src/app/
|
||||
RUN pip install --use-wheel -r requirements.txt
|
||||
ADD . /usr/src/app/
|
||||
RUN pip install .
|
||||
|
||||
|
||||
VOLUME /data/
|
||||
|
||||
RUN mkdir /senpy-plugins/
|
||||
|
||||
WORKDIR /senpy-plugins/
|
||||
ONBUILD ADD . /senpy-plugins/
|
||||
ONBUILD RUN python -m senpy --only-install -f /senpy-plugins
|
||||
|
||||
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]
|
21
Dockerfile-3.4
Normal file
@@ -0,0 +1,21 @@
|
||||
from python:3.4
|
||||
|
||||
RUN mkdir /cache/
|
||||
ENV PIP_CACHE_DIR=/cache/
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ADD requirements.txt /usr/src/app/
|
||||
RUN pip install --use-wheel -r requirements.txt
|
||||
ADD . /usr/src/app/
|
||||
RUN pip install .
|
||||
|
||||
|
||||
VOLUME /data/
|
||||
|
||||
RUN mkdir /senpy-plugins/
|
||||
|
||||
WORKDIR /senpy-plugins/
|
||||
ONBUILD ADD . /senpy-plugins/
|
||||
ONBUILD RUN python -m senpy --only-install -f /senpy-plugins
|
||||
|
||||
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]
|
21
Dockerfile-3.5
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM python:3.5
|
||||
|
||||
RUN mkdir /cache/
|
||||
ENV PIP_CACHE_DIR=/cache/
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ADD requirements.txt /usr/src/app/
|
||||
RUN pip install --use-wheel -r requirements.txt
|
||||
ADD . /usr/src/app/
|
||||
RUN pip install .
|
||||
|
||||
|
||||
VOLUME /data/
|
||||
|
||||
RUN mkdir /senpy-plugins/
|
||||
|
||||
WORKDIR /senpy-plugins/
|
||||
ONBUILD ADD . /senpy-plugins/
|
||||
ONBUILD RUN python -m senpy --only-install -f /senpy-plugins
|
||||
|
||||
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]
|
33
Dockerfile.deps
Normal file
@@ -0,0 +1,33 @@
|
||||
from python:2.7
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get -y install git
|
||||
RUN mkdir -p /senpy-plugins
|
||||
|
||||
RUN apt-get -y install python-numpy
|
||||
RUN apt-get -y install python-scipy
|
||||
RUN apt-get -y install python-sklearn
|
||||
RUN apt-get -y install python-gevent
|
||||
RUN apt-get -y install libopenblas-dev
|
||||
RUN apt-get -y install gfortran
|
||||
RUN apt-get -y install libxml2-dev libxslt1-dev python-dev
|
||||
|
||||
#RUN pip install --upgrade pip
|
||||
|
||||
ADD id_rsa /root/.ssh/id_rsa
|
||||
RUN chmod 700 /root/.ssh/id_rsa
|
||||
RUN echo "Host github.com\n\tStrictHostKeyChecking no\n" >> /root/.ssh/config
|
||||
|
||||
RUN git clone https://github.com/gsi-upm/senpy /usr/src/app/
|
||||
RUN git clone git@github.com:gsi-upm/senpy-plugins-enterprise /senpy-plugins/enterprise
|
||||
RUN git clone https://github.com/gsi-upm/senpy-plugins-community /senpy-plugins/community
|
||||
|
||||
RUN pip install /usr/src/app
|
||||
RUN pip install --no-use-wheel -r /senpy-plugins/enterprise/requirements.txt
|
||||
RUN python -m nltk.downloader stopwords
|
||||
RUN python -m nltk.downloader punkt
|
||||
RUN python -m nltk.downloader maxent_treebank_pos_tagger
|
||||
RUN python -m nltk.downloader wordnet
|
||||
|
||||
WORKDIR /senpy-plugins
|
||||
ENTRYPOINT ["python", "-m", "senpy", "-f", ".", "--host", "0.0.0.0"]
|
21
Dockerfile.template
Normal file
@@ -0,0 +1,21 @@
|
||||
from python:{{PYVERSION}}
|
||||
|
||||
RUN mkdir /cache/
|
||||
ENV PIP_CACHE_DIR=/cache/
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ADD requirements.txt /usr/src/app/
|
||||
RUN pip install --use-wheel -r requirements.txt
|
||||
ADD . /usr/src/app/
|
||||
RUN pip install .
|
||||
|
||||
|
||||
VOLUME /data/
|
||||
|
||||
RUN mkdir /senpy-plugins/
|
||||
|
||||
WORKDIR /senpy-plugins/
|
||||
ONBUILD ADD . /senpy-plugins/
|
||||
ONBUILD RUN python -m senpy --only-install -f /senpy-plugins
|
||||
|
||||
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]
|
9
MANIFEST.in
Normal 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
|
102
Makefile
Normal file
@@ -0,0 +1,102 @@
|
||||
PYVERSIONS=3.5 3.4 2.7
|
||||
PYMAIN=$(firstword $(PYVERSIONS))
|
||||
NAME=senpy
|
||||
REPO=gsiupm
|
||||
VERSION=$(shell ./version.sh)
|
||||
TARNAME=$(NAME)-$(VERSION).tar.gz
|
||||
IMAGENAME=$(REPO)/$(NAME):$(VERSION)
|
||||
TEST_COMMAND=gitlab-runner exec docker --cache-dir=/tmp/gitlabrunner --docker-volumes /tmp/gitlabrunner:/tmp/gitlabrunner --env CI_PROJECT_NAME=$(NAME)
|
||||
|
||||
all: build run
|
||||
|
||||
FORCE:
|
||||
|
||||
version: FORCE
|
||||
@echo $(VERSION) > $(NAME)/VERSION
|
||||
@echo $(VERSION)
|
||||
|
||||
yapf:
|
||||
yapf -i -r senpy
|
||||
yapf -i -r tests
|
||||
|
||||
dev:
|
||||
pip install --user pre-commit
|
||||
pre-commit install
|
||||
|
||||
dockerfiles: $(addprefix Dockerfile-,$(PYVERSIONS))
|
||||
@unlink Dockerfile >/dev/null
|
||||
ln -s Dockerfile-$(PYMAIN) Dockerfile
|
||||
|
||||
Dockerfile-%: Dockerfile.template
|
||||
sed "s/{{PYVERSION}}/$*/" Dockerfile.template > Dockerfile-$*
|
||||
|
||||
quick_build: $(addprefix build-, $(PYMAIN))
|
||||
|
||||
build: $(addprefix build-, $(PYVERSIONS))
|
||||
|
||||
build-%: Dockerfile-%
|
||||
docker build -t '$(IMAGENAME)-python$*' -f Dockerfile-$* .;
|
||||
|
||||
quick_test: $(addprefix test-,$(PYMAIN))
|
||||
|
||||
test: $(addprefix test-,$(PYVERSIONS))
|
||||
|
||||
debug-%:
|
||||
@docker start $(NAME)-debug || (\
|
||||
$(MAKE) build-$*; \
|
||||
docker run -d -w /usr/src/app/ -v $$PWD:/usr/src/app --entrypoint=/bin/bash -p 5000:5000 -ti --name $(NAME)-debug '$(IMAGENAME)-python$*'; \
|
||||
docker exec -ti $(NAME)-debug pip install -r test-requirements.txt; \
|
||||
)\
|
||||
|
||||
docker attach $(NAME)-debug
|
||||
|
||||
debug: debug-$(PYMAIN)
|
||||
|
||||
test-%:
|
||||
$(TEST_COMMAND) test-$*
|
||||
|
||||
dist/$(TARNAME):
|
||||
docker run --rm -ti -v $$PWD:/usr/src/app/ -w /usr/src/app/ python:$(PYMAIN) python setup.py sdist;
|
||||
|
||||
sdist: dist/$(TARNAME)
|
||||
|
||||
pip_test-%: sdist
|
||||
docker run --rm -v $$PWD/dist:/dist/ -ti python:$* pip install /dist/$(TARNAME);
|
||||
|
||||
pip_test: $(addprefix pip_test-,$(PYVERSIONS))
|
||||
|
||||
upload-%: test-%
|
||||
docker push '$(IMAGENAME)-python$*'
|
||||
|
||||
upload: test $(addprefix upload-,$(PYVERSIONS))
|
||||
docker tag '$(IMAGENAME)-python$(PYMAIN)' '$(IMAGENAME)'
|
||||
docker tag '$(IMAGENAME)-python$(PYMAIN)' '$(REPO)/$(NAME)'
|
||||
docker push '$(IMAGENAME)'
|
||||
docker push '$(REPO)/$(NAME)'
|
||||
|
||||
clean:
|
||||
@docker ps -a | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1;}}' | xargs docker rm -v 2>/dev/null|| true
|
||||
@docker images | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1":"$$2;}}' | xargs docker rmi 2>/dev/null|| true
|
||||
@docker rmi $(NAME)-debug 2>/dev/null || true
|
||||
|
||||
|
||||
git_commit:
|
||||
git commit -a
|
||||
|
||||
git_tag:
|
||||
git tag ${VERSION}
|
||||
|
||||
upload_git:
|
||||
git push --tags origin master
|
||||
|
||||
pip_upload:
|
||||
python setup.py sdist upload ;
|
||||
|
||||
pip_test: $(addprefix pip_test-,$(PYVERSIONS))
|
||||
|
||||
run-%: build-%
|
||||
docker run --rm -p 5000:5000 -ti '$(IMAGENAME)-python$(PYMAIN)' --default-plugins
|
||||
|
||||
run: run-$(PYMAIN)
|
||||
|
||||
.PHONY: test test-% build-% build test pip_test run yapf dev
|
2
Procfile
@@ -1 +1 @@
|
||||
web: gunicorn app:app --log-file=-
|
||||
web: python -m senpy --host 0.0.0.0 --port $PORT --default-plugins
|
||||
|
19
README.md
@@ -1,19 +0,0 @@
|
||||

|
||||
[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
|
||||
|
||||

|
||||

|
91
README.rst
Normal 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 --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``
|
||||
|
||||
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
@@ -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
@@ -0,0 +1 @@
|
||||
mock
|
1
docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
_build
|
177
docs/Makefile
Normal 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
@@ -0,0 +1 @@
|
||||
../../senpy/schemas/
|
208
docs/api.rst
Normal file
@@ -0,0 +1,208 @@
|
||||
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`).
|
||||
|
||||
: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
|
||||
|
||||
.. 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": {
|
||||
...
|
||||
},
|
||||
"sentiment140": {
|
||||
"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"
|
||||
},
|
||||
"rand": {
|
||||
"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",
|
||||
"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.
|
||||
|
||||
.. http:get:: /api/plugins/<pluginname>/{de}activate
|
||||
|
||||
{De}activate a plugin.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/plugins/rand/deactivate HTTP/1.1
|
||||
Host: localhost
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
{
|
||||
"@context": {},
|
||||
"message": "Ok"
|
||||
}
|
4
docs/bad-examples/plugins/noplugins.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"plugins": [
|
||||
]
|
||||
}
|
18
docs/bad-examples/results/example-basic-FAIL.json
Normal 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
@@ -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
|
75
docs/demo.rst
Normal 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
|
5
docs/examples/plugins/noplugins.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"@type": "plugins",
|
||||
"plugins": [
|
||||
]
|
||||
}
|
19
docs/examples/results/example-basic.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
88
docs/examples/results/example-complete.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
42
docs/examples/results/example-emotion.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
45
docs/examples/results/example-ner.json
Normal 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": [
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
46
docs/examples/results/example-pad.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
40
docs/examples/results/example-sentiment.json
Normal 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": [
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
37
docs/examples/results/example-suggestion.json
Normal 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": [
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
19
docs/index.rst
Normal file
@@ -0,0 +1,19 @@
|
||||
.. Senpy documentation master file, created by
|
||||
sphinx-quickstart on Tue Feb 24 08:57:32 2015.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to Senpy's documentation!
|
||||
=================================
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
senpy
|
||||
installation
|
||||
usage
|
||||
api
|
||||
schema
|
||||
plugins
|
||||
demo
|
||||
:maxdepth: 2
|
27
docs/installation.rst
Normal 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``
|
246
docs/plugins.rst
Normal file
@@ -0,0 +1,246 @@
|
||||
Developing new plugins
|
||||
----------------------
|
||||
Each plugin represents a different analysis process.There are two types of files that are needed by senpy for loading a plugin:
|
||||
|
||||
- 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
@@ -0,0 +1 @@
|
||||
sphinxcontrib-httpdomain>=1.4
|
74
docs/schema.rst
Normal 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
After Width: | Height: | Size: 65 KiB |
BIN
docs/senpy-playground.png
Normal file
After Width: | Height: | Size: 79 KiB |
35
docs/senpy.rst
Normal 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
|
75
docs/usage.rst
Normal file
@@ -0,0 +1,75 @@
|
||||
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_.
|
||||
|
||||
Video example
|
||||
=============
|
||||
|
||||
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 a plugin.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import requests
|
||||
import json
|
||||
|
||||
r = requests.get('http://127.0.0.1:5000/api/?algo=rand&i=Testing')
|
||||
response = r.content.decode('utf-8')
|
||||
response_json = json.loads(response)
|
||||
|
||||
|
||||
|
||||
.. _section: http://senpy.readthedocs.org/en/latest/api.html
|
||||
|
BIN
img/eu-flag.jpg
Normal file
After Width: | Height: | Size: 5.6 KiB |
828
img/final-logo.svg
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
img/gsi.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
img/header.png
Normal file
After Width: | Height: | Size: 208 KiB |
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
2728
img/logo.svg
Normal file
After Width: | Height: | Size: 180 KiB |
BIN
img/logo_grande.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
img/me.png
Normal file
After Width: | Height: | Size: 25 KiB |
@@ -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
|
||||
|
@@ -14,19 +14,16 @@
|
||||
# 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 __future__ import print_function
|
||||
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']
|
||||
|
@@ -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
|
||||
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__)
|
||||
sp = Senpy()
|
||||
sp.init_app(app)
|
||||
app.debug = True
|
||||
app.run()
|
||||
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()
|
||||
|
124
senpy/api.py
Normal file
@@ -0,0 +1,124 @@
|
||||
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
|
||||
},
|
||||
"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
|
@@ -14,139 +14,127 @@
|
||||
# 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]
|
||||
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
|
||||
|
||||
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
|
||||
})
|
||||
@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:
|
||||
request.params = api_params
|
||||
response = f(*args, **kwargs)
|
||||
except Error as ex:
|
||||
response = ex
|
||||
|
||||
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
|
||||
dic = Plugins(plugins=list(sp.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
@@ -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()
|
35
senpy/client.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import requests
|
||||
import logging
|
||||
from . import models
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Client(object):
|
||||
def __init__(self, endpoint):
|
||||
self.endpoint = endpoint
|
||||
|
||||
def analyse(self, input, method='GET', **kwargs):
|
||||
return self.request('/', method=method, input=input, **kwargs)
|
||||
|
||||
def request(self, path=None, method='GET', **params):
|
||||
url = '{}{}'.format(self.endpoint, path)
|
||||
response = requests.request(method=method, url=url, params=params)
|
||||
try:
|
||||
resp = models.from_dict(response.json())
|
||||
resp.validate(resp)
|
||||
return resp
|
||||
except Exception as ex:
|
||||
logger.error(('There seems to be a problem with the response:\n'
|
||||
'\tURL: {url}\n'
|
||||
'\tError: {error}\n'
|
||||
'\t\n'
|
||||
'#### Response:\n'
|
||||
'\tCode: {code}'
|
||||
'\tContent: {content}'
|
||||
'\n').format(
|
||||
error=ex,
|
||||
url=url,
|
||||
code=response.status_code,
|
||||
content=response.content))
|
||||
raise ex
|
@@ -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#"
|
||||
}
|
@@ -1,114 +1,381 @@
|
||||
"""
|
||||
Main class for Senpy.
|
||||
It orchestrates plugin (de)activation and analysis.
|
||||
"""
|
||||
from future import standard_library
|
||||
standard_library.install_aliases()
|
||||
|
||||
from .plugins import SentimentPlugin, 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 fnmatch
|
||||
import inspect
|
||||
import sys
|
||||
import importlib
|
||||
import imp
|
||||
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
|
||||
return tmp
|
||||
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 = i.emotions.copy()
|
||||
else:
|
||||
newemotions = []
|
||||
for j in i.emotions:
|
||||
for k in candidate.convert(j, fromModel, toModel, params):
|
||||
k.prov__wasGeneratedBy = candidate.id
|
||||
if output == 'nested':
|
||||
k.prov__wasDerivedFrom = j
|
||||
newemotions.append(k)
|
||||
i.emotions = newemotions
|
||||
newentries.append(i)
|
||||
resp.entries = newentries
|
||||
resp.analysis.append(candidate.id)
|
||||
|
||||
@property
|
||||
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:
|
||||
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:
|
||||
deact()
|
||||
else:
|
||||
th = Thread(target=deact)
|
||||
th.start()
|
||||
|
||||
@classmethod
|
||||
def validate_info(cls, info):
|
||||
return all(x in info for x in ('name', 'module', 'version'))
|
||||
|
||||
def install_deps(self):
|
||||
for i in self.plugins.values():
|
||||
self._install_deps(i._info)
|
||||
|
||||
@classmethod
|
||||
def _install_deps(cls, info=None):
|
||||
requirements = info.get('requirements', [])
|
||||
if requirements:
|
||||
pip_args = []
|
||||
pip_args.append('install')
|
||||
pip_args.append('--use-wheel')
|
||||
for req in requirements:
|
||||
pip_args.append(req)
|
||||
logger.info('Installing requirements: ' + str(requirements))
|
||||
pip.main(pip_args)
|
||||
|
||||
@classmethod
|
||||
def _load_plugin_from_info(cls, info, root):
|
||||
if not cls.validate_info(info):
|
||||
logger.warn('The module info is not valid.\n\t{}'.format(info))
|
||||
return None, None
|
||||
module = info["module"]
|
||||
name = info["name"]
|
||||
sys.path.append(root)
|
||||
(fp, pathname, desc) = imp.find_module(module, [root, ])
|
||||
cls._install_deps(info)
|
||||
tmp = imp.load_module(module, fp, pathname, desc)
|
||||
sys.path.remove(root)
|
||||
candidate = None
|
||||
for _, obj in inspect.getmembers(tmp):
|
||||
if inspect.isclass(obj) and inspect.getmodule(obj) == tmp:
|
||||
logger.debug(("Found plugin class:"
|
||||
" {}@{}").format(obj, inspect.getmodule(obj)))
|
||||
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 """
|
||||
|
||||
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 not kwargs:
|
||||
return self.plugins
|
||||
else:
|
||||
return {n: p for n, p in self.plugins.items() if matches(p)}
|
||||
|
||||
def sentiment_plugins(self):
|
||||
""" Return only the sentiment plugins """
|
||||
return {
|
||||
p: plugin
|
||||
for p, plugin in self.plugins.items()
|
||||
if isinstance(plugin, SentimentPlugin)
|
||||
}
|
||||
|
370
senpy/models.py
@@ -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')
|
||||
|
103
senpy/plugins.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from future import standard_library
|
||||
standard_library.install_aliases()
|
||||
|
||||
import inspect
|
||||
import os.path
|
||||
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 analyse(self, *args, **kwargs):
|
||||
raise NotImplemented(
|
||||
'Your method should implement either analyse or analyse_entry')
|
||||
|
||||
def analyse_entry(self, entry, parameters):
|
||||
""" An implemented plugin should override this method.
|
||||
This base method is here to adapt old style plugins which only
|
||||
implement the *analyse* function.
|
||||
Note that this method may yield an annotated entry or a list of
|
||||
entries (e.g. in a tokenizer)
|
||||
"""
|
||||
text = entry['text']
|
||||
params = copy.copy(parameters)
|
||||
params['input'] = text
|
||||
results = self.analyse(**params)
|
||||
for i in results.entries:
|
||||
yield i
|
||||
|
||||
def activate(self):
|
||||
pass
|
||||
|
||||
def deactivate(self):
|
||||
pass
|
||||
|
||||
|
||||
class SentimentPlugin(models.SentimentPlugin, SenpyPlugin):
|
||||
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, SenpyPlugin):
|
||||
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, SenpyPlugin):
|
||||
def __init__(self, info, *args, **kwargs):
|
||||
super(EmotionConversionPlugin, self).__init__(info, *args, **kwargs)
|
||||
|
||||
|
||||
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']:
|
||||
self.shelf_file = os.path.join(tempfile.gettempdir(),
|
||||
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)
|
@@ -1,54 +0,0 @@
|
||||
class SenpyPlugin(object):
|
||||
def __init__(self, name=None, version=None, params=None):
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.params = params or []
|
||||
|
||||
def analyse(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def activate(self):
|
||||
pass
|
||||
|
||||
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
|
||||
|
||||
def jsonable(self, *args, **kwargs):
|
||||
resp = super(SentimentPlugin, self).jsonable(*args, **kwargs)
|
||||
resp["marl:maxPolarityValue"] = self.maxPolarityValue
|
||||
resp["marl:minPolarityValue"] = self.minPolarityValue
|
||||
return resp
|
||||
|
||||
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 jsonable(self, *args, **kwargs):
|
||||
resp = super(EmotionPlugin, self).jsonable(*args, **kwargs)
|
||||
resp["onyx:minEmotionValue"] = self.minEmotionValue
|
||||
resp["onyx:maxEmotionValue"] = self.maxEmotionValue
|
||||
return resp
|
56
senpy/plugins/conversion/emotion/ekman2vad.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from senpy.plugins import EmotionConversionPlugin
|
||||
from senpy.models import EmotionSet, Emotion, Error
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import math
|
||||
|
||||
|
||||
class WNA2VAD(EmotionConversionPlugin):
|
||||
|
||||
def _ekman_to_vad(self, ekmanSet):
|
||||
potency = 0
|
||||
arousal = 0
|
||||
dominance = 0
|
||||
for e in ekmanSet.onyx__hasEmotion:
|
||||
category = e.onyx__hasEmotionCategory
|
||||
centroid = self.centroids[category]
|
||||
potency += centroid['V']
|
||||
arousal += centroid['A']
|
||||
dominance += centroid['D']
|
||||
e = Emotion({'emoml:potency': potency,
|
||||
'emoml:arousal': arousal,
|
||||
'emoml:dominance': dominance})
|
||||
return e
|
||||
|
||||
def _vad_to_ekman(self, VADEmotion):
|
||||
V = VADEmotion['emoml:valence']
|
||||
A = VADEmotion['emoml:potency']
|
||||
D = VADEmotion['emoml:dominance']
|
||||
emotion = ''
|
||||
value = 10000000000000000000000.0
|
||||
for state in self.centroids:
|
||||
valence = V - self.centroids[state]['V']
|
||||
arousal = A - self.centroids[state]['A']
|
||||
dominance = D - self.centroids[state]['D']
|
||||
new_value = math.sqrt((valence**2) +
|
||||
(arousal**2) +
|
||||
(dominance**2))
|
||||
if new_value < value:
|
||||
value = new_value
|
||||
emotion = state
|
||||
result = Emotion(onyx__hasEmotionCategory=emotion)
|
||||
return result
|
||||
|
||||
def convert(self, emotionSet, fromModel, toModel, params):
|
||||
logger.debug('{}\n{}\n{}\n{}'.format(emotionSet, fromModel, toModel, params))
|
||||
e = EmotionSet()
|
||||
if fromModel == 'emoml:big6':
|
||||
e.onyx__hasEmotion.append(self._ekman_to_vad(emotionSet))
|
||||
elif fromModel == 'emoml:fsre-dimensions':
|
||||
for i in emotionSet.onyx__hasEmotion:
|
||||
e.onyx__hasEmotion.append(self._vad_to_ekman(e))
|
||||
else:
|
||||
raise Error('EMOTION MODEL NOT KNOWN')
|
||||
yield e
|
35
senpy/plugins/conversion/emotion/ekman2vad.senpy
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Ekman2VAD
|
||||
module: ekman2vad
|
||||
description: Plugin to convert from Ekman to VAD
|
||||
version: 0.1
|
||||
onyx:doesConversion:
|
||||
- onyx:conversionFrom: emoml:big6
|
||||
onyx:conversionTo: emoml:fsre-dimensions
|
||||
- onyx:conversionFrom: emoml:fsre-dimensions
|
||||
onyx:conversionTo: wna:WNAModel
|
||||
centroids:
|
||||
emoml:big6anger:
|
||||
A: 6.95
|
||||
D: 5.1
|
||||
V: 2.7
|
||||
emoml:big6disgust:
|
||||
A: 5.3
|
||||
D: 8.05
|
||||
V: 2.7
|
||||
emoml:big6fear:
|
||||
A: 6.5
|
||||
D: 3.6
|
||||
V: 3.2
|
||||
emoml:big6happiness:
|
||||
A: 7.22
|
||||
D: 6.28
|
||||
V: 8.6
|
||||
emoml:big6sadness:
|
||||
A: 5.21
|
||||
D: 2.82
|
||||
V: 2.21
|
||||
aliases:
|
||||
A: emoml:arousal
|
||||
V: emoml:potency
|
||||
D: emoml:dominance
|
18
senpy/plugins/example/emoRand/emoRand.py
Normal 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
|
9
senpy/plugins/example/emoRand/emoRand.senpy
Normal 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"
|
24
senpy/plugins/example/rand/rand.py
Normal 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
|
10
senpy/plugins/example/rand/rand.senpy
Normal 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"
|
36
senpy/plugins/sentiment/sentiment140/sentiment140.py
Normal 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
|
21
senpy/plugins/sentiment/sentiment140/sentiment140.senpy
Normal 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
|
@@ -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()
|
@@ -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()
|
15
senpy/schemas/analysis.json
Normal 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
@@ -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"]
|
||||
}
|
5
senpy/schemas/context.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "JSON-LD Context",
|
||||
"type": ["array", "string", "object"]
|
||||
}
|
56
senpy/schemas/context.jsonld
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
45
senpy/schemas/definitions.json
Normal 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"
|
||||
}
|
||||
}
|
9
senpy/schemas/dimensions.json
Normal 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"]
|
||||
}
|
4
senpy/schemas/emotion.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"type": "object"
|
||||
}
|
19
senpy/schemas/emotionAnalysis.json
Normal 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"]
|
||||
}]
|
||||
}
|
12
senpy/schemas/emotionConversion.json
Normal 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"]
|
||||
}
|
19
senpy/schemas/emotionConversionPlugin.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
27
senpy/schemas/emotionModel.json
Normal 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"]
|
||||
}
|
19
senpy/schemas/emotionPlugin.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
24
senpy/schemas/emotionSet.json
Normal 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"]
|
||||
}
|
4
senpy/schemas/entity.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"type": "object"
|
||||
}
|
39
senpy/schemas/entry.json
Normal 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
@@ -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
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
18
senpy/schemas/plugins.json
Normal 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": {
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
9
senpy/schemas/response.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@type": {"type": "string"}
|
||||
},
|
||||
"required": ["@type"]
|
||||
|
||||
}
|
39
senpy/schemas/results.json
Normal 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"]
|
||||
}
|
||||
]
|
||||
}
|
23
senpy/schemas/sentiment.json
Normal 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"]
|
||||
}
|
19
senpy/schemas/sentimentPlugin.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
5
senpy/schemas/suggestion.json
Normal 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
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"type": "object"
|
||||
}
|
347
senpy/static/css/bootstrap-theme.css
vendored
Normal 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 */
|