mirror of
https://github.com/gsi-upm/senpy
synced 2025-09-16 03:22:22 +00:00
Compare commits
42 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
28f29d159a | ||
|
c803f60fd4 | ||
|
12eae16e37 | ||
|
f3372c27b6 | ||
|
b6de72a143 | ||
|
0f89b92457 | ||
|
ea91e3e4a4 | ||
|
f76b777b9f | ||
|
e112dd55ce | ||
|
60ef304108 | ||
|
7927cf1587 | ||
|
13cefbedfb | ||
|
4ba9535d56 | ||
|
e582ef07d4 | ||
|
ef40bdb545 | ||
|
e0b4c76238 | ||
|
14c86ec38c | ||
|
d3d05b3218 | ||
|
eababcadb0 | ||
|
7efece0224 | ||
|
1302b0b93c | ||
|
ad1092690b | ||
|
e35e810ede | ||
|
d5ddcb8d3f | ||
|
54c0c9c437 | ||
|
6e970d01f2 | ||
|
1d0a54ecd2 | ||
|
800d4a9c2c | ||
|
035ef98b7e | ||
|
d7e115d7c2 | ||
|
548cb4c9ba | ||
|
7e5b55ff9c | ||
|
8b2c3e8d40 | ||
|
0c8f98d466 | ||
|
cc298742ec | ||
|
250052fb99 | ||
|
603e086606 | ||
|
a8614bab0c | ||
|
70ca74b03c | ||
|
c9e6d78183 | ||
|
1a582c0843 | ||
|
0394bcd69c |
@@ -17,6 +17,6 @@ WORKDIR /usr/src/app
|
||||
COPY test-requirements.txt requirements.txt /usr/src/app/
|
||||
RUN pip install --use-wheel -r test-requirements.txt -r requirements.txt
|
||||
COPY . /usr/src/app/
|
||||
RUN pip install --no-deps --no-index .
|
||||
RUN pip install --no-index --no-deps --editable .
|
||||
|
||||
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]
|
||||
|
15
Makefile
15
Makefile
@@ -6,6 +6,7 @@ VERSION=$(shell git describe --tags --dirty 2>/dev/null)
|
||||
TARNAME=$(NAME)-$(VERSION).tar.gz
|
||||
IMAGENAME=$(REPO)/$(NAME)
|
||||
IMAGEWTAG=$(IMAGENAME):$(VERSION)
|
||||
DEVPORT=5000
|
||||
action="test-${PYMAIN}"
|
||||
|
||||
all: build run
|
||||
@@ -43,7 +44,7 @@ quick_test: $(addprefix test-,$(PYMAIN))
|
||||
dev-%:
|
||||
@docker start $(NAME)-dev$* || (\
|
||||
$(MAKE) build-$*; \
|
||||
docker run -d -w /usr/src/app/ -v $$PWD:/usr/src/app --entrypoint=/bin/bash -ti --name $(NAME)-dev$* '$(IMAGEWTAG)-python$*'; \
|
||||
docker run -d -w /usr/src/app/ -p $(DEVPORT):5000 -v $$PWD:/usr/src/app --entrypoint=/bin/bash -ti --name $(NAME)-dev$* '$(IMAGEWTAG)-python$*'; \
|
||||
)\
|
||||
|
||||
docker exec -ti $(NAME)-dev$* bash
|
||||
@@ -57,8 +58,10 @@ test-%: build-%
|
||||
|
||||
test: test-$(PYMAIN)
|
||||
|
||||
dist/$(TARNAME):
|
||||
dist/$(TARNAME): version
|
||||
docker run --rm -ti -v $$PWD:/usr/src/app/ -w /usr/src/app/ python:$(PYMAIN) python setup.py sdist;
|
||||
docker run --rm -ti -v $$PWD:/usr/src/app/ -w /usr/src/app/ python:$(PYMAIN) chmod -R a+rwx dist;
|
||||
|
||||
|
||||
sdist: dist/$(TARNAME)
|
||||
|
||||
@@ -70,8 +73,8 @@ pip_test: $(addprefix pip_test-,$(PYVERSIONS))
|
||||
clean:
|
||||
@docker ps -a | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1;}}' | xargs docker rm -v 2>/dev/null|| true
|
||||
@docker images | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1":"$$2;}}' | xargs docker rmi 2>/dev/null|| true
|
||||
@docker rmi $(NAME)-dev 2>/dev/null || true
|
||||
|
||||
@docker stop $(addprefix $(NAME)-dev,$(PYVERSIONS)) 2>/dev/null || true
|
||||
@docker rm $(addprefix $(NAME)-dev,$(PYVERSIONS)) 2>/dev/null || true
|
||||
|
||||
git_commit:
|
||||
git commit -a
|
||||
@@ -82,11 +85,11 @@ git_tag:
|
||||
git_push:
|
||||
git push --tags origin master
|
||||
|
||||
pip_upload:
|
||||
pip_upload: pip_test
|
||||
python setup.py sdist upload ;
|
||||
|
||||
run-%: build-%
|
||||
docker run --rm -p 5000:5000 -ti '$(IMAGEWTAG)-python$(PYMAIN)' --default-plugins
|
||||
docker run --rm -p $(DEVPORT):5000 -ti '$(IMAGEWTAG)-python$(PYMAIN)' --default-plugins
|
||||
|
||||
run: run-$(PYMAIN)
|
||||
|
||||
|
51
README.rst
51
README.rst
@@ -23,7 +23,7 @@ Through PIP
|
||||
|
||||
.. code:: bash
|
||||
|
||||
pip install --user senpy
|
||||
pip install -U --user senpy
|
||||
|
||||
|
||||
Alternatively, you can use the development version:
|
||||
@@ -42,6 +42,53 @@ Build the image or use the pre-built one: ``docker run -ti -p 5000:5000 gsiupm/s
|
||||
|
||||
To add custom plugins, add a volume and tell senpy where to find the plugins: ``docker run -ti -p 5000:5000 -v <PATH OF PLUGINS>:/plugins gsiupm/senpy --default-plugins -f /plugins``
|
||||
|
||||
|
||||
Developing
|
||||
----------
|
||||
|
||||
Developing/debugging
|
||||
********************
|
||||
This command will run the senpy container using the latest image available, mounting your current folder so you get your latest code:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
|
||||
# Python 3.5
|
||||
make dev
|
||||
# Python 2.7
|
||||
make dev-2.7
|
||||
|
||||
Building a docker image
|
||||
***********************
|
||||
|
||||
.. code:: bash
|
||||
|
||||
|
||||
# Python 3.5
|
||||
make build-3.5
|
||||
# Python 2.7
|
||||
make build-2.7
|
||||
|
||||
Testing
|
||||
*******
|
||||
|
||||
.. code:: bash
|
||||
|
||||
|
||||
make test
|
||||
|
||||
Running
|
||||
*******
|
||||
This command will run the senpy server listening on localhost:5000
|
||||
|
||||
.. code:: bash
|
||||
|
||||
|
||||
# Python 3.5
|
||||
make run-3.5
|
||||
# Python 2.7
|
||||
make run-2.7
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
@@ -49,12 +96,14 @@ However, the easiest and recommended way is to just use the command-line tool to
|
||||
|
||||
.. code:: bash
|
||||
|
||||
|
||||
senpy
|
||||
|
||||
or, alternatively:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
|
||||
python -m senpy
|
||||
|
||||
|
||||
|
BIN
docs/_static/header.png
vendored
Normal file
BIN
docs/_static/header.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 208 KiB |
237
docs/api.rst
237
docs/api.rst
@@ -1,5 +1,5 @@
|
||||
NIF API
|
||||
=======
|
||||
-------
|
||||
.. http:get:: /api
|
||||
|
||||
Basic endpoint for sentiment/emotion analysis.
|
||||
@@ -22,38 +22,32 @@ NIF API
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
"@context":"http://127.0.0.1/api/contexts/Results.jsonld",
|
||||
"@id":"_:Results_11241245.22",
|
||||
"@type":"results"
|
||||
"analysis": [
|
||||
"plugins/sentiment-140_0.1"
|
||||
],
|
||||
"entries": [
|
||||
{
|
||||
"@id": "_:Entry_11241245.22"
|
||||
"@type":"entry",
|
||||
"emotions": [],
|
||||
"entities": [],
|
||||
"sentiments": [
|
||||
{
|
||||
"@id": "Sentiment0",
|
||||
"@type": "sentiment",
|
||||
"marl:hasPolarity": "marl:Negative",
|
||||
"marl:polarityValue": 0,
|
||||
"prefix": ""
|
||||
}
|
||||
],
|
||||
"suggestions": [],
|
||||
"text": "This text makes me sad.\nwhilst this text makes me happy and surprised at the same time.\nI cannot believe it!",
|
||||
"topics": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query i input: No default. Depends on informat and intype
|
||||
@@ -92,58 +86,59 @@ NIF API
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
{
|
||||
"@context": {
|
||||
...
|
||||
},
|
||||
"@type": "plugins",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "sentiment140",
|
||||
"is_activated": true,
|
||||
"version": "0.1",
|
||||
"extra_params": {
|
||||
"@id": "extra_params_sentiment140_0.1",
|
||||
"language": {
|
||||
"required": false,
|
||||
"@id": "lang_sentiment140",
|
||||
"options": [
|
||||
"es",
|
||||
"en",
|
||||
"auto"
|
||||
],
|
||||
"aliases": [
|
||||
"language",
|
||||
"l"
|
||||
]
|
||||
}
|
||||
},
|
||||
"@id": "sentiment140_0.1"
|
||||
}, {
|
||||
"name": "rand",
|
||||
"is_activated": true,
|
||||
"version": "0.1",
|
||||
"extra_params": {
|
||||
"@id": "extra_params_rand_0.1",
|
||||
"language": {
|
||||
"required": false,
|
||||
"@id": "lang_rand",
|
||||
"options": [
|
||||
"es",
|
||||
"en",
|
||||
"auto"
|
||||
],
|
||||
"aliases": [
|
||||
"language",
|
||||
"l"
|
||||
]
|
||||
}
|
||||
},
|
||||
"@id": "rand_0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
{
|
||||
"@id": "plugins/sentiment-140_0.1",
|
||||
"@type": "sentimentPlugin",
|
||||
"author": "@balkian",
|
||||
"description": "Sentiment classifier using rule-based classification for English and Spanish. This plugin uses sentiment140 data to perform classification. For more information: http://help.sentiment140.com/for-students/",
|
||||
"extra_params": {
|
||||
"language": {
|
||||
"@id": "lang_sentiment140",
|
||||
"aliases": [
|
||||
"language",
|
||||
"l"
|
||||
],
|
||||
"options": [
|
||||
"es",
|
||||
"en",
|
||||
"auto"
|
||||
],
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"is_activated": true,
|
||||
"maxPolarityValue": 1.0,
|
||||
"minPolarityValue": 0.0,
|
||||
"module": "sentiment-140",
|
||||
"name": "sentiment-140",
|
||||
"requirements": {},
|
||||
"version": "0.1"
|
||||
},
|
||||
{
|
||||
"@id": "plugins/ExamplePlugin_0.1",
|
||||
"@type": "sentimentPlugin",
|
||||
"author": "@balkian",
|
||||
"custom_attribute": "42",
|
||||
"description": "I am just an example",
|
||||
"extra_params": {
|
||||
"parameter": {
|
||||
"@id": "parameter",
|
||||
"aliases": [
|
||||
"parameter",
|
||||
"param"
|
||||
],
|
||||
"default": 42,
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"is_activated": true,
|
||||
"maxPolarityValue": 1.0,
|
||||
"minPolarityValue": 0.0,
|
||||
"module": "example",
|
||||
"name": "ExamplePlugin",
|
||||
"requirements": "noop",
|
||||
"version": "0.1"
|
||||
}
|
||||
|
||||
.. http:get:: /api/plugins/<pluginname>
|
||||
|
||||
@@ -162,30 +157,60 @@ NIF API
|
||||
.. sourcecode:: http
|
||||
|
||||
{
|
||||
"@id": "rand_0.1",
|
||||
"@type": "sentimentPlugin",
|
||||
"extra_params": {
|
||||
"@id": "extra_params_rand_0.1",
|
||||
"language": {
|
||||
"@id": "lang_rand",
|
||||
"aliases": [
|
||||
"language",
|
||||
"l"
|
||||
],
|
||||
"options": [
|
||||
"es",
|
||||
"en",
|
||||
"auto"
|
||||
],
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"is_activated": true,
|
||||
"name": "rand",
|
||||
"version": "0.1"
|
||||
"@context": "http://127.0.0.1/api/contexts/ExamplePlugin.jsonld",
|
||||
"@id": "plugins/ExamplePlugin_0.1",
|
||||
"@type": "sentimentPlugin",
|
||||
"author": "@balkian",
|
||||
"custom_attribute": "42",
|
||||
"description": "I am just an example",
|
||||
"extra_params": {
|
||||
"parameter": {
|
||||
"@id": "parameter",
|
||||
"aliases": [
|
||||
"parameter",
|
||||
"param"
|
||||
],
|
||||
"default": 42,
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"is_activated": true,
|
||||
"maxPolarityValue": 1.0,
|
||||
"minPolarityValue": 0.0,
|
||||
"module": "example",
|
||||
"name": "ExamplePlugin",
|
||||
"requirements": "noop",
|
||||
"version": "0.1"
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.. http:get:: /api/plugins/default
|
||||
|
||||
Return the information about the default plugin.
|
||||
|
7
docs/apischema.rst
Normal file
7
docs/apischema.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
API and Schema
|
||||
##############
|
||||
.. toctree::
|
||||
|
||||
vocabularies.rst
|
||||
api.rst
|
||||
schema.rst
|
15
docs/architecture.rst
Normal file
15
docs/architecture.rst
Normal file
@@ -0,0 +1,15 @@
|
||||
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
|
||||
:width: 100%
|
||||
:align: center
|
78
docs/bad-examples/results/example-analysis-as-id-FAIL.json
Normal file
78
docs/bad-examples/results/example-analysis-as-id-FAIL.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
|
||||
"@id": "me:Result1",
|
||||
"@type": "results",
|
||||
"analysis": [
|
||||
"me:SAnalysis1",
|
||||
"me:SgAnalysis1",
|
||||
"me:EmotionAnalysis1",
|
||||
"me:NER1",
|
||||
{
|
||||
"@type": "analysis",
|
||||
"@id": "wrong"
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
38
docs/conf.py
38
docs/conf.py
@@ -37,6 +37,7 @@ extensions = [
|
||||
'sphinx.ext.todo',
|
||||
'sphinxcontrib.httpdomain',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.autosectionlabel'
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
@@ -54,20 +55,21 @@ master_doc = 'index'
|
||||
# General information about the project.
|
||||
project = u'Senpy'
|
||||
copyright = u'2016, J. Fernando Sánchez'
|
||||
description = u'A framework for sentiment and emotion analysis services'
|
||||
|
||||
# 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()
|
||||
# with open('../senpy/VERSION') as f:
|
||||
# version = f.read().strip()
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = version
|
||||
# release = version
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
@@ -104,14 +106,14 @@ pygments_style = 'sphinx'
|
||||
#keep_warnings = False
|
||||
|
||||
|
||||
html_theme = 'alabaster'
|
||||
# -- 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()]
|
||||
# if not on_rtd: # only import and set the theme if we're building docs locally
|
||||
# import sphinx_rtd_theme
|
||||
# html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
||||
else:
|
||||
html_theme = 'default'
|
||||
# else:
|
||||
# html_theme = 'default'
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
@@ -119,7 +121,13 @@ else:
|
||||
# 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 = {}
|
||||
html_theme_options = {
|
||||
'logo': 'header.png',
|
||||
'github_user': 'gsi-upm',
|
||||
'github_repo': 'senpy',
|
||||
'github_banner': True,
|
||||
}
|
||||
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
@@ -159,7 +167,13 @@ html_static_path = ['_static']
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
html_sidebars = {
|
||||
'**': [
|
||||
'about.html',
|
||||
'navigation.html',
|
||||
'searchbox.html',
|
||||
]
|
||||
}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
|
@@ -1,7 +1,8 @@
|
||||
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.
|
||||
There is a demo available on http://senpy.cluster.gsi.dit.upm.es/, where you can test a serie of different plugins.
|
||||
You can use the playground (a web interface) or make HTTP requests to the service API.
|
||||
|
||||
.. image:: senpy-playground.png
|
||||
:height: 400px
|
||||
@@ -12,64 +13,4 @@ There is a demo available on http://senpy.demos.gsi.dit.upm.es/, where you can a
|
||||
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
|
||||
The source code and description of the plugins used in the demo is available here: https://lab.cluster.gsi.dit.upm.es/senpy/senpy-plugins-community/.
|
||||
|
74
docs/examples/results/example-analysis-as-id.json
Normal file
74
docs/examples/results/example-analysis-as-id.json
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
|
||||
"@id": "me:Result1",
|
||||
"@type": "results",
|
||||
"analysis": [
|
||||
"me:SAnalysis1",
|
||||
"me:SgAnalysis1",
|
||||
"me:EmotionAnalysis1",
|
||||
"me:NER1"
|
||||
],
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@@ -1,15 +1,28 @@
|
||||
Welcome to Senpy's documentation!
|
||||
=================================
|
||||
|
||||
Contents:
|
||||
With Senpy, you can easily turn your sentiment or emotion analysis algorithm into a full blown semantic service.
|
||||
Sharing your sentiment analysis with the world has never been easier.
|
||||
Senpy provides:
|
||||
|
||||
* Parameter validation, error handling
|
||||
* Formatting: JSON-LD, Turtle/n-triples input and output, or simple text input
|
||||
* Linked Data. Results are semantically annotated, using a series of well established vocabularies, and sane default URIs.
|
||||
* A web UI where users can explore your service and test different settings
|
||||
* A client to interact with any senpy service
|
||||
* A command line tool
|
||||
|
||||
|
||||
|
||||
.. toctree::
|
||||
:caption: Learn more about senpy
|
||||
:maxdepth: 2
|
||||
|
||||
senpy
|
||||
installation
|
||||
usage
|
||||
api
|
||||
schema
|
||||
apischema
|
||||
plugins
|
||||
conversion
|
||||
demo
|
||||
:maxdepth: 2
|
||||
research.rst
|
||||
|
@@ -22,6 +22,35 @@ 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``.
|
||||
Build the image or use the pre-built one:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
docker run -ti -p 5000:5000 gsiupm/senpy --host 0.0.0.0 --default-plugins
|
||||
|
||||
To add custom plugins, use a docker volume:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
docker run -ti -p 5000:5000 -v <PATH OF PLUGINS>:/plugins gsiupm/senpy --host 0.0.0.0 --default-plugins -f /plugins
|
||||
|
||||
Alias
|
||||
.....
|
||||
|
||||
If you are using the docker approach regularly, it is advisable to use a script or an alias to simplify your executions:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
alias senpy='docker run --rm -ti -p 5000:5000 -v $PWD:/senpy-plugins gsiupm/senpy --default-plugins'
|
||||
|
||||
|
||||
Python 2
|
||||
........
|
||||
|
||||
There is a Senpy version for python2 too:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
docker run -ti -p 5000:5000 gsiupm/senpy:python2.7 --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``
|
||||
|
@@ -2,27 +2,34 @@ Developing new plugins
|
||||
----------------------
|
||||
This document describes how to develop a new analysis plugin. For an example of conversion plugins, see :doc:`conversion`.
|
||||
|
||||
Each plugin represents a different analysis process.There are two types of files that are needed by senpy for loading a plugin:
|
||||
A more step-by-step tutorial with slides is available `here <https://lab.cluster.gsi.dit.upm.es/senpy/senpy-tutorial>`__
|
||||
|
||||
- Definition file, has the ".senpy" extension.
|
||||
- Code file, is a python file.
|
||||
What is a plugin?
|
||||
=================
|
||||
|
||||
This separation will allow us to deploy plugins that use the same code but employ different parameters.
|
||||
A plugin is a program that, given a text, will add annotations to it.
|
||||
In practice, a plugin consists of at least two files:
|
||||
|
||||
- Definition file: a `.senpy` file that describes the plugin (e.g. what input parameters it accepts, what emotion model it uses).
|
||||
- Python module: the actual code that will add annotations to each input.
|
||||
|
||||
This separation allows 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
|
||||
===================
|
||||
Plugin Definition files
|
||||
=======================
|
||||
|
||||
The definition file contains all the attributes of the plugin, and can be written in YAML or JSON.
|
||||
When the server is launched, it will recursively search for definition files in the plugin folder (the current folder, by default).
|
||||
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:
|
||||
* extra_params: to add parameters to the senpy API when this plugin is requested. Those parameters may be required, and have aliased names. For instance:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
@@ -68,10 +75,28 @@ 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.
|
||||
* analyse_entry: called in every user requests. It takes two parameters: ``Entry``, the entry object, and ``params``, the parameters supplied by the user. It 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.
|
||||
|
||||
Entries
|
||||
=======
|
||||
|
||||
Entries are objects that can be annotated.
|
||||
By default, entries are `NIF contexts <http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core/nif-core.html>`_ represented in JSON-LD format.
|
||||
Annotations are added to the object like this:
|
||||
|
||||
.. code:: python
|
||||
|
||||
entry = Entry()
|
||||
entry.vocabulary__annotationName = 'myvalue'
|
||||
entry['vocabulary:annotationName'] = 'myvalue'
|
||||
entry['annotationNameURI'] = 'myvalue'
|
||||
|
||||
Where vocabulary is one of the prefixes defined in the default senpy context, and annotationURI is a full URI.
|
||||
The value may be any valid JSON-LD dictionary.
|
||||
For simplicity, senpy includes a series of models by default in the ``senpy.models`` module.
|
||||
|
||||
|
||||
Example plugin
|
||||
==============
|
||||
@@ -117,6 +142,13 @@ Now, in a file named ``helloworld.py``:
|
||||
|
||||
F.A.Q.
|
||||
======
|
||||
What annotations can I use?
|
||||
???????????????????????????
|
||||
|
||||
You can add almost any annotation to an entry.
|
||||
The most common use cases are covered in the :doc:`schema`.
|
||||
|
||||
|
||||
Why does the analyse function yield instead of return?
|
||||
??????????????????????????????????????????????????????
|
||||
|
||||
@@ -151,7 +183,11 @@ Training a classifier can be time time consuming. To avoid running the training
|
||||
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.
|
||||
You can specify 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.
|
||||
|
||||
Shelves may get corrupted if the plugin exists unexpectedly.
|
||||
A corrupt shelf prevents the plugin from loading.
|
||||
If you do not care about the pickle, you can force your plugin to remove the corrupted file and load anyway, set the 'force_shelf' to True in your .senpy file.
|
||||
|
||||
I want to implement my service as a plugin, How i can do it?
|
||||
????????????????????????????????????????????????????????????
|
||||
|
11
docs/research.rst
Normal file
11
docs/research.rst
Normal file
@@ -0,0 +1,11 @@
|
||||
Research
|
||||
--------
|
||||
|
||||
If you use Senpy in your research, please cite `Senpy: A Pragmatic Linked Sentiment Analysis Framework <http://gsi.dit.upm.es/index.php/es/investigacion/publicaciones?view=publication&task=show&id=417>`__ (`BibTex <http://gsi.dit.upm.es/index.php/es/investigacion/publicaciones?controller=publications&task=export&format=bibtex&id=417>`__):
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
Sánchez-Rada, J. F., Iglesias, C. A., Corcuera, I., & Araque, Ó. (2016, October).
|
||||
Senpy: A Pragmatic Linked Sentiment Analysis Framework.
|
||||
In Data Science and Advanced Analytics (DSAA),
|
||||
2016 IEEE International Conference on (pp. 735-742). IEEE.
|
@@ -1,74 +1,74 @@
|
||||
Schema Examples
|
||||
===============
|
||||
Schema
|
||||
------
|
||||
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
|
||||
,,,,,,,,,,,,,,
|
||||
.. literalinclude:: examples/results/example-basic.json
|
||||
:language: json-ld
|
||||
|
||||
Sentiment Analysis
|
||||
---------------------
|
||||
.....................
|
||||
Description
|
||||
...........
|
||||
,,,,,,,,,,,
|
||||
|
||||
Representation
|
||||
..............
|
||||
,,,,,,,,,,,,,,
|
||||
|
||||
.. literalinclude:: examples/example-sentiment.json
|
||||
.. literalinclude:: examples/results/example-sentiment.json
|
||||
:emphasize-lines: 5-10,25-33
|
||||
:language: json-ld
|
||||
|
||||
Suggestion Mining
|
||||
-----------------
|
||||
.................
|
||||
Description
|
||||
...........
|
||||
,,,,,,,,,,,
|
||||
|
||||
Representation
|
||||
..............
|
||||
,,,,,,,,,,,,,,
|
||||
|
||||
.. literalinclude:: examples/example-suggestion.json
|
||||
.. literalinclude:: examples/results/example-suggestion.json
|
||||
:emphasize-lines: 5-8,22-27
|
||||
:language: json-ld
|
||||
|
||||
Emotion Analysis
|
||||
----------------
|
||||
................
|
||||
Description
|
||||
...........
|
||||
,,,,,,,,,,,
|
||||
|
||||
Representation
|
||||
..............
|
||||
,,,,,,,,,,,,,,
|
||||
|
||||
.. literalinclude:: examples/example-emotion.json
|
||||
.. literalinclude:: examples/results/example-emotion.json
|
||||
:language: json-ld
|
||||
:emphasize-lines: 5-8,25-37
|
||||
|
||||
Named Entity Recognition
|
||||
------------------------
|
||||
........................
|
||||
Description
|
||||
...........
|
||||
,,,,,,,,,,,
|
||||
|
||||
Representation
|
||||
..............
|
||||
,,,,,,,,,,,,,,
|
||||
|
||||
.. literalinclude:: examples/example-ner.json
|
||||
.. literalinclude:: examples/results/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
|
||||
.. literalinclude:: examples/results/example-complete.json
|
||||
:language: json-ld
|
||||
|
@@ -1,35 +1,32 @@
|
||||
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.
|
||||
Senpy is a framework that turns your sentiment or emotion analysis algorithm into a full blown semantic service.
|
||||
Senpy takes care of:
|
||||
|
||||
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.
|
||||
* Interfacing with the user: parameter validation, error handling.
|
||||
* Formatting: JSON-LD, Turtle/n-triples input and output, or simple text input
|
||||
* Linked Data: senpy results are semantically annotated, using a series of well established vocabularies, and sane default URIs.
|
||||
* User interface: a web UI where users can explore your service and test different settings
|
||||
* A client to interact with the service. Currently only available in Python.
|
||||
|
||||
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.
|
||||
Sharing your sentiment analysis with the world has never been easier!
|
||||
|
||||
Specifications
|
||||
==============
|
||||
Senpy for service developers
|
||||
============================
|
||||
|
||||
The model used in Senpy is based on the following specifications:
|
||||
Check out the :doc:`plugins` if you have developed an analysis algorithm (e.g. sentiment analysis) and you want to publish it as a service.
|
||||
|
||||
* 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
|
||||
Senpy for end users
|
||||
===================
|
||||
|
||||
Architecture
|
||||
============
|
||||
All services built using senpy share a common interface.
|
||||
This allows users to use them (almost) interchangeably.
|
||||
Senpy comes with a :ref:`built-in client`.
|
||||
|
||||
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:
|
||||
.. toctree::
|
||||
:caption: Interested? Check out senpy's:
|
||||
|
||||
architecture
|
||||
|
||||
* 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
|
||||
|
@@ -1,20 +1,9 @@
|
||||
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.
|
||||
First of all, you need to install the package.
|
||||
See :doc:`installation` for installation instructions.
|
||||
Once installed, the `senpy` command should be available.
|
||||
|
||||
Useful command-line options
|
||||
===========================
|
||||
@@ -23,19 +12,19 @@ In case you want to load modules, which are located in different folders under t
|
||||
|
||||
.. code:: bash
|
||||
|
||||
python -m senpy -f .
|
||||
senpy -f .
|
||||
|
||||
The default port used by senpy is 5000, but you can change it using the option `--port`.
|
||||
The default port used by senpy is 5000, but you can change it using the `--port` flag.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
python -m senpy --port 8080
|
||||
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
|
||||
senpy --host 0.0.0.0
|
||||
|
||||
For more options, see the `--help` page.
|
||||
|
||||
@@ -48,15 +37,19 @@ Once the server is launched, there is a basic endpoint in the server, which prov
|
||||
|
||||
In case you want to know the different endpoints of the server, there is more information available in the NIF API section_.
|
||||
|
||||
CLI
|
||||
===
|
||||
CLI demo
|
||||
========
|
||||
|
||||
This video shows how to use senpy through command-line tool.
|
||||
|
||||
https://asciinema.org/a/9uwef1ghkjk062cw2t4mhzpyk
|
||||
.. image:: https://asciinema.org/a/9uwef1ghkjk062cw2t4mhzpyk.png
|
||||
:width: 100%
|
||||
:target: https://asciinema.org/a/9uwef1ghkjk062cw2t4mhzpyk
|
||||
:alt: CLI demo
|
||||
|
||||
Request example in python
|
||||
=========================
|
||||
|
||||
Built-in client
|
||||
===============
|
||||
|
||||
This example shows how to make a request to the default plugin:
|
||||
|
||||
|
8
docs/vocabularies.rst
Normal file
8
docs/vocabularies.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
Vocabularies and model
|
||||
======================
|
||||
|
||||
The model used in Senpy is based on the following vocabularies:
|
||||
|
||||
* 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
|
@@ -1,6 +1,6 @@
|
||||
Flask>=0.10.1
|
||||
requests>=2.4.1
|
||||
gevent>=1.1rc4
|
||||
tornado>=4.4.3
|
||||
PyLD>=0.6.5
|
||||
six
|
||||
future
|
||||
|
@@ -22,35 +22,19 @@ the server.
|
||||
|
||||
from flask import Flask
|
||||
from senpy.extensions import Senpy
|
||||
from gevent.wsgi import WSGIServer
|
||||
from gevent.monkey import patch_all
|
||||
from tornado.wsgi import WSGIContainer
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.ioloop import IOLoop
|
||||
|
||||
|
||||
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(
|
||||
@@ -94,28 +78,41 @@ def main():
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Do not run a server, only install plugin dependencies')
|
||||
parser.add_argument(
|
||||
'--version',
|
||||
'-v',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Output the senpy version and exit')
|
||||
args = parser.parse_args()
|
||||
if args.version:
|
||||
print('Senpy version {}'.format(senpy.__version__))
|
||||
exit(1)
|
||||
logging.basicConfig()
|
||||
rl = logging.getLogger()
|
||||
rl.setLevel(getattr(logging, args.level))
|
||||
app = Flask(__name__)
|
||||
app.debug = args.debug
|
||||
if args.debug:
|
||||
sys.excepthook = info
|
||||
sp = Senpy(app, args.plugins_folder, default_plugins=args.default_plugins)
|
||||
if args.only_install:
|
||||
sp.install_deps()
|
||||
return
|
||||
sp.activate_all()
|
||||
http_server = WSGIServer((args.host, args.port), app)
|
||||
try:
|
||||
print('Senpy version {}'.format(senpy.__version__))
|
||||
print('Server running on port %s:%d. Ctrl+C to quit' % (args.host,
|
||||
args.port))
|
||||
http_server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print('Bye!')
|
||||
http_server.stop()
|
||||
print('Senpy version {}'.format(senpy.__version__))
|
||||
print('Server running on port %s:%d. Ctrl+C to quit' % (args.host,
|
||||
args.port))
|
||||
if not app.debug:
|
||||
http_server = HTTPServer(WSGIContainer(app))
|
||||
http_server.listen(args.port, address=args.host)
|
||||
try:
|
||||
IOLoop.instance().start()
|
||||
except KeyboardInterrupt:
|
||||
print('Bye!')
|
||||
http_server.stop()
|
||||
else:
|
||||
app.run(args.host,
|
||||
args.port,
|
||||
debug=True)
|
||||
sp.deactivate_all()
|
||||
|
||||
|
||||
|
@@ -26,6 +26,13 @@ API_PARAMS = {
|
||||
"aliases": ["emotionModel", "emoModel"],
|
||||
"required": False
|
||||
},
|
||||
"plugin_type": {
|
||||
"@id": "pluginType",
|
||||
"description": 'What kind of plugins to list',
|
||||
"aliases": ["pluginType", "plugin_type"],
|
||||
"required": True,
|
||||
"default": "analysisPlugin"
|
||||
},
|
||||
"conversion": {
|
||||
"@id": "conversion",
|
||||
"description": "How to show the elements that have (not) been converted",
|
||||
@@ -63,7 +70,7 @@ NIF_PARAMS = {
|
||||
"aliases": ["f", "informat"],
|
||||
"required": False,
|
||||
"default": "text",
|
||||
"options": ["turtle", "text"],
|
||||
"options": ["turtle", "text", "json-ld"],
|
||||
},
|
||||
"intype": {
|
||||
"@id": "intype",
|
||||
|
@@ -25,6 +25,7 @@ from .version import __version__
|
||||
from functools import wraps
|
||||
|
||||
import logging
|
||||
import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -74,7 +75,7 @@ def basic_api(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
raw_params = get_params(request)
|
||||
headers = {'X-ORIGINAL-PARAMS': raw_params}
|
||||
headers = {'X-ORIGINAL-PARAMS': json.dumps(raw_params)}
|
||||
# Get defaults
|
||||
web_params = parse_params({}, spec=WEB_PARAMS)
|
||||
api_params = parse_params({}, spec=API_PARAMS)
|
||||
@@ -92,6 +93,9 @@ def basic_api(f):
|
||||
response = f(*args, **kwargs)
|
||||
except Error as ex:
|
||||
response = ex
|
||||
logger.error(ex)
|
||||
if current_app.debug:
|
||||
raise
|
||||
|
||||
in_headers = web_params['inHeaders'] != "0"
|
||||
expanded = api_params['expanded-jsonld']
|
||||
@@ -121,7 +125,9 @@ def api():
|
||||
@basic_api
|
||||
def plugins():
|
||||
sp = current_app.senpy
|
||||
dic = Plugins(plugins=list(sp.plugins.values()))
|
||||
ptype = request.params.get('plugin_type')
|
||||
plugins = sp.filter_plugins(plugin_type=ptype)
|
||||
dic = Plugins(plugins=list(plugins.values()))
|
||||
return dic
|
||||
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import requests
|
||||
import logging
|
||||
from . import models
|
||||
from .plugins import default_plugin_type
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -12,6 +13,10 @@ class Client(object):
|
||||
def analyse(self, input, method='GET', **kwargs):
|
||||
return self.request('/', method=method, input=input, **kwargs)
|
||||
|
||||
def plugins(self, ptype=default_plugin_type):
|
||||
resp = self.request(path='/plugins', plugin_type=ptype).plugins
|
||||
return {p.name: p for p in resp}
|
||||
|
||||
def request(self, path=None, method='GET', **params):
|
||||
url = '{}{}'.format(self.endpoint, path)
|
||||
response = requests.request(method=method, url=url, params=params)
|
||||
|
@@ -5,8 +5,9 @@ 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 . import plugins
|
||||
from .plugins import SenpyPlugin
|
||||
from .models import Error, Entry, Results, from_string
|
||||
from .blueprints import api_blueprint, demo_blueprint, ns_blueprint
|
||||
from .api import API_PARAMS, NIF_PARAMS, parse_params
|
||||
|
||||
@@ -21,11 +22,18 @@ import importlib
|
||||
import logging
|
||||
import traceback
|
||||
import yaml
|
||||
import pip
|
||||
import subprocess
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def log_subprocess_output(process):
|
||||
for line in iter(process.stdout.readline, b''):
|
||||
logger.info('%r', line)
|
||||
for line in iter(process.stderr.readline, b''):
|
||||
logger.error('%r', line)
|
||||
|
||||
|
||||
class Senpy(object):
|
||||
""" Default Senpy extension for Flask """
|
||||
|
||||
@@ -77,74 +85,105 @@ class Senpy(object):
|
||||
else:
|
||||
logger.debug("Not a folder: %s", folder)
|
||||
|
||||
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:
|
||||
def _find_plugins(self, params):
|
||||
if not self.analysis_plugins:
|
||||
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()))
|
||||
" Please install one."))
|
||||
api_params = parse_params(params, spec=API_PARAMS)
|
||||
algos = None
|
||||
if "algorithm" in api_params and api_params["algorithm"]:
|
||||
algos = api_params["algorithm"].split(',')
|
||||
elif self.default_plugin:
|
||||
algos = [self.default_plugin.name, ]
|
||||
else:
|
||||
raise Error(
|
||||
status=404,
|
||||
message="The algorithm '{}' is not valid".format(algo))
|
||||
message="No default plugin found, and None provided")
|
||||
|
||||
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]
|
||||
plugins = list()
|
||||
for algo in algos:
|
||||
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 _get_params(self, params, plugin):
|
||||
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))
|
||||
plugins.append(self.plugins[algo])
|
||||
return plugins
|
||||
|
||||
def _get_params(self, params, plugin=None):
|
||||
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)
|
||||
if plugin:
|
||||
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':
|
||||
results = Results()
|
||||
entry = Entry(text=params['input'])
|
||||
results.entries.append(entry)
|
||||
elif params['informat'] == 'json-ld':
|
||||
results = from_string(params['input'], cls=Results)
|
||||
else:
|
||||
raise NotImplemented('Only text input format implemented')
|
||||
yield entry
|
||||
raise NotImplemented('Informat {} is not implemented'.format(params['informat']))
|
||||
return results
|
||||
|
||||
def _process_entries(self, entries, plugins, nif_params):
|
||||
if not plugins:
|
||||
for i in entries:
|
||||
yield i
|
||||
return
|
||||
plugin = plugins[0]
|
||||
specific_params = self._get_params(nif_params, plugin)
|
||||
results = plugin.analyse_entries(entries, specific_params)
|
||||
for i in self._process_entries(results, plugins[1:], nif_params):
|
||||
yield i
|
||||
|
||||
def _process_response(self, resp, plugins, nif_params):
|
||||
entries = resp.entries
|
||||
resp.entries = []
|
||||
for plug in plugins:
|
||||
resp.analysis.append(plug.id)
|
||||
for i in self._process_entries(entries, plugins, nif_params):
|
||||
resp.entries.append(i)
|
||||
return resp
|
||||
|
||||
def analyse(self, **api_params):
|
||||
"""
|
||||
Main method that analyses a request, either from CLI or HTTP.
|
||||
It uses a dictionary of parameters, provided by the user.
|
||||
"""
|
||||
logger.debug("analysing with params: {}".format(api_params))
|
||||
plugin = self._find_plugin(api_params)
|
||||
nif_params = self._get_params(api_params, plugin)
|
||||
resp = Results()
|
||||
plugins = self._find_plugins(api_params)
|
||||
nif_params = self._get_params(api_params)
|
||||
resp = self._get_entries(nif_params)
|
||||
if 'with_parameters' in api_params:
|
||||
resp.parameters = nif_params
|
||||
try:
|
||||
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)
|
||||
resp = self._process_response(resp, plugins, nif_params)
|
||||
self.convert_emotions(resp, plugins, nif_params)
|
||||
logger.debug("Returning analysis result: {}".format(resp))
|
||||
except Error as ex:
|
||||
except (Error, Exception) as ex:
|
||||
if not isinstance(ex, Error):
|
||||
ex = Error(message=str(ex), status=500)
|
||||
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)
|
||||
raise ex
|
||||
return resp
|
||||
|
||||
def _conversion_candidates(self, fromModel, toModel):
|
||||
candidates = self.filter_plugins(**{'@type': 'emotionConversionPlugin'})
|
||||
candidates = self.filter_plugins(plugin_type='emotionConversionPlugin')
|
||||
for name, candidate in candidates.items():
|
||||
for pair in candidate.onyx__doesConversion:
|
||||
logging.debug(pair)
|
||||
@@ -154,30 +193,32 @@ class Senpy(object):
|
||||
# logging.debug('Found candidate: {}'.format(candidate))
|
||||
yield candidate
|
||||
|
||||
def convert_emotions(self, resp, plugin, params):
|
||||
def convert_emotions(self, resp, plugins, params):
|
||||
"""
|
||||
Conversion of all emotions in a response.
|
||||
Conversion of all emotions in a response **in place**.
|
||||
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
|
||||
|
||||
logger.debug('Asked for model: {}'.format(toModel))
|
||||
output = params.get('conversion', None)
|
||||
candidates = {}
|
||||
for plugin in plugins:
|
||||
try:
|
||||
fromModel = plugin.get('onyx:usesEmotionModel', None)
|
||||
candidates[plugin.id] = next(self._conversion_candidates(fromModel, toModel))
|
||||
logger.debug('Analysis plugin {} uses model: {}'.format(plugin.id, fromModel))
|
||||
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":
|
||||
@@ -185,6 +226,9 @@ class Senpy(object):
|
||||
else:
|
||||
newemotions = []
|
||||
for j in i.emotions:
|
||||
plugname = j['prov:wasGeneratedBy']
|
||||
candidate = candidates[plugname]
|
||||
resp.analysis.append(candidate.id)
|
||||
for k in candidate.convert(j, fromModel, toModel, params):
|
||||
k.prov__wasGeneratedBy = candidate.id
|
||||
if output == 'nested':
|
||||
@@ -193,13 +237,14 @@ class Senpy(object):
|
||||
i.emotions = newemotions
|
||||
newentries.append(i)
|
||||
resp.entries = newentries
|
||||
resp.analysis.append(candidate.id)
|
||||
resp.analysis = list(set(resp.analysis))
|
||||
|
||||
@property
|
||||
def default_plugin(self):
|
||||
candidate = self._default
|
||||
if not candidate:
|
||||
candidates = self.filter_plugins(is_activated=True)
|
||||
candidates = self.filter_plugins(plugin_type='analysisPlugin',
|
||||
is_activated=True)
|
||||
if len(candidates) > 0:
|
||||
candidate = list(candidates.values())[0]
|
||||
logger.debug("Default: {}".format(candidate))
|
||||
@@ -258,6 +303,7 @@ class Senpy(object):
|
||||
else:
|
||||
th = Thread(target=act)
|
||||
th.start()
|
||||
return th
|
||||
|
||||
def deactivate_plugin(self, plugin_name, sync=False):
|
||||
try:
|
||||
@@ -282,6 +328,7 @@ class Senpy(object):
|
||||
else:
|
||||
th = Thread(target=deact)
|
||||
th.start()
|
||||
return th
|
||||
|
||||
@classmethod
|
||||
def validate_info(cls, info):
|
||||
@@ -295,13 +342,19 @@ class Senpy(object):
|
||||
def _install_deps(cls, info=None):
|
||||
requirements = info.get('requirements', [])
|
||||
if requirements:
|
||||
pip_args = []
|
||||
pip_args = ['pip']
|
||||
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)
|
||||
process = subprocess.Popen(pip_args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
log_subprocess_output(process)
|
||||
exitcode = process.wait()
|
||||
if exitcode != 0:
|
||||
raise Error("Dependencies not properly installed")
|
||||
|
||||
@classmethod
|
||||
def _load_module(cls, name, root):
|
||||
@@ -366,23 +419,9 @@ class Senpy(object):
|
||||
return self._plugin_list
|
||||
|
||||
def filter_plugins(self, **kwargs):
|
||||
""" Filter plugins by different criteria """
|
||||
return plugins.pfilter(self.plugins, **kwargs)
|
||||
|
||||
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)
|
||||
}
|
||||
@property
|
||||
def analysis_plugins(self):
|
||||
""" Return only the analysis plugins """
|
||||
return self.filter_plugins(plugin_type='analysisPlugin')
|
||||
|
@@ -214,6 +214,7 @@ class BaseModel(SenpyMixin, dict):
|
||||
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):
|
||||
@@ -236,7 +237,10 @@ class BaseModel(SenpyMixin, dict):
|
||||
self.__setitem__(self._get_key(key), value)
|
||||
|
||||
def __delattr__(self, key):
|
||||
self.__delitem__(self._get_key(key))
|
||||
try:
|
||||
object.__delattr__(self, key)
|
||||
except AttributeError:
|
||||
self.__delitem__(self._get_key(key))
|
||||
|
||||
def _plain_dict(self):
|
||||
d = {k: v for (k, v) in self.items() if k[0] != "_"}
|
||||
@@ -252,13 +256,32 @@ 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_dict(indict, cls=None):
|
||||
if not cls:
|
||||
target = indict.get('@type', None)
|
||||
try:
|
||||
if target and target in _subtypes:
|
||||
cls = _subtypes[target]
|
||||
else:
|
||||
cls = BaseModel
|
||||
except Exception:
|
||||
cls = BaseModel
|
||||
outdict = dict()
|
||||
for k, v in indict.items():
|
||||
if k == '@context':
|
||||
pass
|
||||
elif isinstance(v, dict):
|
||||
v = from_dict(indict[k])
|
||||
elif isinstance(v, list):
|
||||
for ix, v2 in enumerate(v):
|
||||
if isinstance(v2, dict):
|
||||
v[ix] = from_dict(v2)
|
||||
outdict[k] = v
|
||||
return cls(**outdict)
|
||||
|
||||
|
||||
def from_string(string, **kwargs):
|
||||
return from_dict(json.loads(string), **kwargs)
|
||||
|
||||
|
||||
def from_json(injson):
|
||||
@@ -308,7 +331,7 @@ for i in [
|
||||
_ErrorModel = from_schema('error')
|
||||
|
||||
|
||||
class Error(SenpyMixin, BaseException):
|
||||
class Error(SenpyMixin, Exception):
|
||||
def __init__(self, message, *args, **kwargs):
|
||||
super(Error, self).__init__(self, message, message)
|
||||
self._error = _ErrorModel(message=message, *args, **kwargs)
|
||||
@@ -337,5 +360,8 @@ class Error(SenpyMixin, BaseException):
|
||||
def __delattr__(self, key):
|
||||
delattr(self._error, key)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.to_JSON(with_context=False))
|
||||
|
||||
|
||||
register(Error, 'error')
|
||||
|
@@ -9,11 +9,12 @@ import logging
|
||||
import tempfile
|
||||
import copy
|
||||
from .. import models
|
||||
from ..api import API_PARAMS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SenpyPlugin(models.Plugin):
|
||||
class Plugin(models.Plugin):
|
||||
def __init__(self, info=None):
|
||||
"""
|
||||
Provides a canonical name for plugins and serves as base for other
|
||||
@@ -24,12 +25,24 @@ class SenpyPlugin(models.Plugin):
|
||||
"information for the plugin."))
|
||||
logger.debug("Initialising {}".format(info))
|
||||
id = 'plugins/{}_{}'.format(info['name'], info['version'])
|
||||
super(SenpyPlugin, self).__init__(id=id, **info)
|
||||
super(Plugin, self).__init__(id=id, **info)
|
||||
self.is_activated = False
|
||||
|
||||
def get_folder(self):
|
||||
return os.path.dirname(inspect.getfile(self.__class__))
|
||||
|
||||
def activate(self):
|
||||
pass
|
||||
|
||||
def deactivate(self):
|
||||
pass
|
||||
|
||||
|
||||
SenpyPlugin = Plugin
|
||||
|
||||
|
||||
class AnalysisPlugin(Plugin):
|
||||
|
||||
def analyse(self, *args, **kwargs):
|
||||
raise NotImplemented(
|
||||
'Your method should implement either analyse or analyse_entry')
|
||||
@@ -48,30 +61,33 @@ class SenpyPlugin(models.Plugin):
|
||||
for i in results.entries:
|
||||
yield i
|
||||
|
||||
def activate(self):
|
||||
pass
|
||||
|
||||
def deactivate(self):
|
||||
pass
|
||||
def analyse_entries(self, entries, parameters):
|
||||
for entry in entries:
|
||||
logger.debug('Analysing entry with plugin {}: {}'.format(self, entry))
|
||||
for result in self.analyse_entry(entry, parameters):
|
||||
yield result
|
||||
|
||||
|
||||
class SentimentPlugin(models.SentimentPlugin, SenpyPlugin):
|
||||
class ConversionPlugin(Plugin):
|
||||
pass
|
||||
|
||||
|
||||
class SentimentPlugin(models.SentimentPlugin, AnalysisPlugin):
|
||||
def __init__(self, info, *args, **kwargs):
|
||||
super(SentimentPlugin, self).__init__(info, *args, **kwargs)
|
||||
self.minPolarityValue = float(info.get("minPolarityValue", 0))
|
||||
self.maxPolarityValue = float(info.get("maxPolarityValue", 1))
|
||||
|
||||
|
||||
class EmotionPlugin(models.EmotionPlugin, SenpyPlugin):
|
||||
class EmotionPlugin(models.EmotionPlugin, AnalysisPlugin):
|
||||
def __init__(self, info, *args, **kwargs):
|
||||
super(EmotionPlugin, self).__init__(info, *args, **kwargs)
|
||||
self.minEmotionValue = float(info.get("minEmotionValue", -1))
|
||||
self.maxEmotionValue = float(info.get("maxEmotionValue", 1))
|
||||
|
||||
|
||||
class EmotionConversionPlugin(models.EmotionConversionPlugin, SenpyPlugin):
|
||||
def __init__(self, info, *args, **kwargs):
|
||||
super(EmotionConversionPlugin, self).__init__(info, *args, **kwargs)
|
||||
class EmotionConversionPlugin(models.EmotionConversionPlugin, ConversionPlugin):
|
||||
pass
|
||||
|
||||
|
||||
class ShelfMixin(object):
|
||||
@@ -80,7 +96,12 @@ class ShelfMixin(object):
|
||||
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'))
|
||||
try:
|
||||
self.__dict__['_sh'] = pickle.load(open(self.shelf_file, 'rb'))
|
||||
except (IndexError, EOFError, pickle.UnpicklingError):
|
||||
logger.warning('{} has a corrupted shelf file!'.format(self.id))
|
||||
if not self.get('force_shelf', False):
|
||||
raise
|
||||
return self._sh
|
||||
|
||||
@sh.deleter
|
||||
@@ -102,3 +123,40 @@ class ShelfMixin(object):
|
||||
if hasattr(self, '_sh') and self._sh is not None:
|
||||
with open(self.shelf_file, 'wb') as f:
|
||||
pickle.dump(self._sh, f)
|
||||
|
||||
|
||||
default_plugin_type = API_PARAMS['plugin_type']['default']
|
||||
|
||||
|
||||
def pfilter(plugins, **kwargs):
|
||||
""" Filter plugins by different criteria """
|
||||
if isinstance(plugins, models.Plugins):
|
||||
plugins = plugins.plugins
|
||||
elif isinstance(plugins, dict):
|
||||
plugins = plugins.values()
|
||||
ptype = kwargs.pop('plugin_type', default_plugin_type)
|
||||
logger.debug('#' * 100)
|
||||
logger.debug('ptype {}'.format(ptype))
|
||||
if ptype:
|
||||
try:
|
||||
ptype = ptype[0].upper() + ptype[1:]
|
||||
pclass = globals()[ptype]
|
||||
logger.debug('Class: {}'.format(pclass))
|
||||
candidates = filter(lambda x: isinstance(x, pclass),
|
||||
plugins)
|
||||
except KeyError:
|
||||
raise models.Error('{} is not a valid type'.format(ptype))
|
||||
else:
|
||||
candidates = plugins
|
||||
|
||||
logger.debug(candidates)
|
||||
|
||||
def matches(plug):
|
||||
res = all(getattr(plug, k, None) == v for (k, v) in kwargs.items())
|
||||
logger.debug(
|
||||
"matching {} with {}: {}".format(plug.name, kwargs, res))
|
||||
return res
|
||||
|
||||
if kwargs:
|
||||
candidates = filter(matches, candidates)
|
||||
return {p.name: p for p in candidates}
|
||||
|
@@ -6,6 +6,33 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CentroidConversion(EmotionConversionPlugin):
|
||||
def __init__(self, info):
|
||||
if 'centroids' not in info:
|
||||
raise Error('Centroid conversion plugins should provide '
|
||||
'the centroids in their senpy file')
|
||||
if 'onyx:doesConversion' not in info:
|
||||
if 'centroids_direction' not in info:
|
||||
raise Error('Please, provide centroids direction')
|
||||
|
||||
cf, ct = info['centroids_direction']
|
||||
info['onyx:doesConversion'] = [{
|
||||
'onyx:conversionFrom': cf,
|
||||
'onyx:conversionTo': ct
|
||||
}, {
|
||||
'onyx:conversionFrom': ct,
|
||||
'onyx:conversionTo': cf
|
||||
}]
|
||||
|
||||
if 'aliases' in info:
|
||||
aliases = info['aliases']
|
||||
ncentroids = {}
|
||||
for k1, v1 in info['centroids'].items():
|
||||
nv1 = {}
|
||||
for k2, v2 in v1.items():
|
||||
nv1[aliases.get(k2, k2)] = v2
|
||||
ncentroids[aliases.get(k1, k1)] = nv1
|
||||
info['centroids'] = ncentroids
|
||||
super(CentroidConversion, self).__init__(info)
|
||||
|
||||
def _forward_conversion(self, original):
|
||||
"""Sum the VAD value of all categories found."""
|
||||
@@ -25,7 +52,7 @@ class CentroidConversion(EmotionConversionPlugin):
|
||||
dimensions = list(self.centroids.values())[0]
|
||||
|
||||
def distance(e1, e2):
|
||||
return sum((e1[k] - e2.get(self.aliases[k], 0)) for k in dimensions)
|
||||
return sum((e1[k] - e2.get(k, 0)) for k in dimensions)
|
||||
|
||||
emotion = ''
|
||||
mindistance = 10000000000000000000000.0
|
||||
@@ -40,11 +67,12 @@ class CentroidConversion(EmotionConversionPlugin):
|
||||
def convert(self, emotionSet, fromModel, toModel, params):
|
||||
|
||||
cf, ct = self.centroids_direction
|
||||
logger.debug('{}\n{}\n{}\n{}'.format(emotionSet, fromModel, toModel, params))
|
||||
logger.debug(
|
||||
'{}\n{}\n{}\n{}'.format(emotionSet, fromModel, toModel, params))
|
||||
e = EmotionSet()
|
||||
if fromModel == cf:
|
||||
if fromModel == cf and toModel == ct:
|
||||
e.onyx__hasEmotion.append(self._forward_conversion(emotionSet))
|
||||
elif fromModel == ct:
|
||||
elif fromModel == ct and toModel == cf:
|
||||
for i in emotionSet.onyx__hasEmotion:
|
||||
e.onyx__hasEmotion.append(self._backwards_conversion(i))
|
||||
else:
|
||||
|
39
senpy/plugins/conversion/emotion/ekman2fsre.senpy
Normal file
39
senpy/plugins/conversion/emotion/ekman2fsre.senpy
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: Ekman2FSRE
|
||||
module: senpy.plugins.conversion.centroids
|
||||
description: Plugin to convert emotion sets from Ekman to VAD
|
||||
version: 0.1
|
||||
# No need to specify onyx:doesConversion because centroids.py adds it automatically from centroids_direction
|
||||
centroids:
|
||||
anger:
|
||||
A: 6.95
|
||||
D: 5.1
|
||||
V: 2.7
|
||||
disgust:
|
||||
A: 5.3
|
||||
D: 8.05
|
||||
V: 2.7
|
||||
fear:
|
||||
A: 6.5
|
||||
D: 3.6
|
||||
V: 3.2
|
||||
happiness:
|
||||
A: 7.22
|
||||
D: 6.28
|
||||
V: 8.6
|
||||
sadness:
|
||||
A: 5.21
|
||||
D: 2.82
|
||||
V: 2.21
|
||||
centroids_direction:
|
||||
- emoml:big6
|
||||
- emoml:fsre-dimensions
|
||||
aliases: # These are aliases for any key in the centroid, to avoid repeating a long name several times
|
||||
A: emoml:arousal
|
||||
V: emoml:valence
|
||||
D: emoml:dominance
|
||||
anger: emoml:big6anger
|
||||
disgust: emoml:big6disgust
|
||||
fear: emoml:big6fear
|
||||
happiness: emoml:big6happiness
|
||||
sadness: emoml:big6sadness
|
@@ -1,38 +1,39 @@
|
||||
---
|
||||
name: Ekman2VAD
|
||||
name: Ekman2PAD
|
||||
module: senpy.plugins.conversion.centroids
|
||||
description: Plugin to convert emotion sets from Ekman to VAD
|
||||
version: 0.1
|
||||
onyx:doesConversion:
|
||||
- onyx:conversionFrom: emoml:big6
|
||||
onyx:conversionTo: emoml:fsre-dimensions
|
||||
- onyx:conversionFrom: emoml:fsre-dimensions
|
||||
onyx:conversionTo: emoml:big6
|
||||
# No need to specify onyx:doesConversion because centroids.py adds it automatically from centroids_direction
|
||||
centroids:
|
||||
emoml:big6anger:
|
||||
anger:
|
||||
A: 6.95
|
||||
D: 5.1
|
||||
V: 2.7
|
||||
emoml:big6disgust:
|
||||
disgust:
|
||||
A: 5.3
|
||||
D: 8.05
|
||||
V: 2.7
|
||||
emoml:big6fear:
|
||||
fear:
|
||||
A: 6.5
|
||||
D: 3.6
|
||||
V: 3.2
|
||||
emoml:big6happiness:
|
||||
happiness:
|
||||
A: 7.22
|
||||
D: 6.28
|
||||
V: 8.6
|
||||
emoml:big6sadness:
|
||||
sadness:
|
||||
A: 5.21
|
||||
D: 2.82
|
||||
V: 2.21
|
||||
centroids_direction:
|
||||
- emoml:big6
|
||||
- emoml:fsre-dimensions
|
||||
aliases:
|
||||
- emoml:pad
|
||||
aliases: # These are aliases for any key in the centroid, to avoid repeating a long name several times
|
||||
A: emoml:arousal
|
||||
V: emoml:valence
|
||||
D: emoml:dominance
|
||||
D: emoml:dominance
|
||||
anger: emoml:big6anger
|
||||
disgust: emoml:big6disgust
|
||||
fear: emoml:big6fear
|
||||
happiness: emoml:big6happiness
|
||||
sadness: emoml:big6sadness
|
@@ -37,6 +37,12 @@
|
||||
"@type": "@id",
|
||||
"@container": "@set"
|
||||
},
|
||||
"options": {
|
||||
"@container": "@set"
|
||||
},
|
||||
"plugins": {
|
||||
"@container": "@set"
|
||||
},
|
||||
"prov:wasGeneratedBy": {
|
||||
"@type": "@id"
|
||||
},
|
||||
|
@@ -6,11 +6,10 @@
|
||||
"properties": {
|
||||
"plugins": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "plugin.json"
|
||||
}
|
||||
},
|
||||
"@type": {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -18,10 +18,16 @@
|
||||
"type": "string"
|
||||
},
|
||||
"analysis": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "analysis.json"
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "analysis.json"
|
||||
},{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"entries": {
|
||||
|
@@ -47,7 +47,7 @@
|
||||
This website is the senpy Playground, which allows you to test the instance of senpy in this server. It provides a user-friendly interface to the functions exposed by the senpy API.
|
||||
</p>
|
||||
<p>
|
||||
Once you get comfortable with the parameters and results, you are encouraged to issue your own requests to the API endpoint, which should be <a href="/api">here</a>.
|
||||
Once you get comfortable with the parameters and results, you are encouraged to issue your own requests to the API endpoint. You can find examples of API URL's when you try out a plugin with the "Analyse!" button on the "Test it" tab.
|
||||
</p>
|
||||
<p>
|
||||
These are some of the things you can do with the API:
|
||||
|
23
tests/plugins/async_plugin/asyncplugin.py
Normal file
23
tests/plugins/async_plugin/asyncplugin.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from senpy.plugins import AnalysisPlugin
|
||||
|
||||
import multiprocessing
|
||||
|
||||
|
||||
def _train(process_number):
|
||||
return process_number
|
||||
|
||||
|
||||
class AsyncPlugin(AnalysisPlugin):
|
||||
def _do_async(self, num_processes):
|
||||
pool = multiprocessing.Pool(processes=num_processes)
|
||||
values = pool.map(_train, range(num_processes))
|
||||
|
||||
return values
|
||||
|
||||
def activate(self):
|
||||
self.value = self._do_async(4)
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
values = self._do_async(2)
|
||||
entry.async_values = values
|
||||
yield entry
|
8
tests/plugins/async_plugin/asyncplugin.senpy
Normal file
8
tests/plugins/async_plugin/asyncplugin.senpy
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
name: Async
|
||||
module: asyncplugin
|
||||
description: I am async
|
||||
author: "@balkian"
|
||||
version: '0.1'
|
||||
async: true
|
||||
extra_params: {}
|
@@ -4,4 +4,5 @@ from senpy.plugins import SentimentPlugin
|
||||
class DummyPlugin(SentimentPlugin):
|
||||
def analyse_entry(self, entry, params):
|
||||
entry.text = entry.text[::-1]
|
||||
entry.reversed = entry.get('reversed', 0) + 1
|
||||
yield entry
|
||||
|
@@ -1,8 +1,8 @@
|
||||
from senpy.plugins import SenpyPlugin
|
||||
from senpy.plugins import AnalysisPlugin
|
||||
from time import sleep
|
||||
|
||||
|
||||
class SleepPlugin(SenpyPlugin):
|
||||
class SleepPlugin(AnalysisPlugin):
|
||||
def activate(self, *args, **kwargs):
|
||||
sleep(self.timeout)
|
||||
|
||||
|
@@ -19,6 +19,7 @@ def parse_resp(resp):
|
||||
class BlueprintsTest(TestCase):
|
||||
def setUp(self):
|
||||
self.app = Flask("test_extensions")
|
||||
self.app.debug = False
|
||||
self.client = self.app.test_client()
|
||||
self.senpy = Senpy()
|
||||
self.senpy.init_app(self.app)
|
||||
|
@@ -4,18 +4,21 @@ try:
|
||||
except ImportError:
|
||||
from mock import patch
|
||||
|
||||
import json
|
||||
|
||||
from senpy.client import Client
|
||||
from senpy.models import Results, Error
|
||||
from senpy.models import Results, Plugins, Error
|
||||
from senpy.plugins import AnalysisPlugin, default_plugin_type
|
||||
|
||||
|
||||
class Call(dict):
|
||||
def __init__(self, obj):
|
||||
self.obj = obj.jsonld()
|
||||
self.obj = obj.serialize()
|
||||
self.status_code = 200
|
||||
self.content = self.json()
|
||||
|
||||
def json(self):
|
||||
return self.obj
|
||||
return json.loads(self.obj)
|
||||
|
||||
|
||||
class ModelsTest(TestCase):
|
||||
@@ -44,3 +47,19 @@ class ModelsTest(TestCase):
|
||||
method='GET',
|
||||
params={'input': 'hello',
|
||||
'algorithm': 'NONEXISTENT'})
|
||||
|
||||
def test_plugins(self):
|
||||
endpoint = 'http://dummy/'
|
||||
client = Client(endpoint)
|
||||
plugins = Plugins()
|
||||
p1 = AnalysisPlugin({'name': 'AnalysisP1', 'version': 0, 'description': 'No'})
|
||||
plugins.plugins = [p1, ]
|
||||
success = Call(plugins)
|
||||
with patch('requests.request', return_value=success) as patched:
|
||||
response = client.plugins()
|
||||
assert isinstance(response, dict)
|
||||
assert len(response) == 1
|
||||
assert "AnalysisP1" in response
|
||||
patched.assert_called_with(
|
||||
url=endpoint + '/plugins', method='GET',
|
||||
params={'plugin_type': default_plugin_type})
|
||||
|
@@ -10,7 +10,7 @@ except ImportError:
|
||||
|
||||
from functools import partial
|
||||
from senpy.extensions import Senpy
|
||||
from senpy.models import Error, Results, Entry, EmotionSet, Emotion
|
||||
from senpy.models import Error, Results, Entry, EmotionSet, Emotion, Plugin
|
||||
from flask import Flask
|
||||
from unittest import TestCase
|
||||
|
||||
@@ -61,6 +61,19 @@ class ExtensionsTest(TestCase):
|
||||
assert len(self.senpy.plugins) >= 3
|
||||
assert self.senpy.plugins["Sleep"].is_activated
|
||||
|
||||
def test_installing_nonexistent(self):
|
||||
""" Fail if the dependencies cannot be met """
|
||||
info = {
|
||||
'name': 'TestPipFail',
|
||||
'module': 'dummy',
|
||||
'description': None,
|
||||
'requirements': ['IAmMakingThisPackageNameUpToFail'],
|
||||
'version': 0
|
||||
}
|
||||
root = os.path.join(self.dir, 'plugins', 'dummy_plugin')
|
||||
with self.assertRaises(Error):
|
||||
name, module = self.senpy._load_plugin_from_info(info, root=root)
|
||||
|
||||
def test_disabling(self):
|
||||
""" Disabling a plugin """
|
||||
self.senpy.deactivate_all(sync=True)
|
||||
@@ -96,19 +109,49 @@ class ExtensionsTest(TestCase):
|
||||
assert r2.analysis[0] == "plugins/Dummy_0.1"
|
||||
assert r1.entries[0].text == 'input'
|
||||
|
||||
def test_analyse_jsonld(self):
|
||||
""" Using a plugin with JSON-LD input"""
|
||||
js_input = '''{
|
||||
"@id": "prueba",
|
||||
"@type": "results",
|
||||
"entries": [
|
||||
{"@id": "entry1",
|
||||
"text": "tupni",
|
||||
"@type": "entry"
|
||||
}
|
||||
]
|
||||
}'''
|
||||
r1 = self.senpy.analyse(algorithm="Dummy",
|
||||
input=js_input,
|
||||
informat="json-ld",
|
||||
output="tuptuo")
|
||||
r2 = self.senpy.analyse(input="tupni", output="tuptuo")
|
||||
assert r1.analysis[0] == "plugins/Dummy_0.1"
|
||||
assert r2.analysis[0] == "plugins/Dummy_0.1"
|
||||
assert r1.entries[0].text == 'input'
|
||||
|
||||
def test_analyse_error(self):
|
||||
mm = mock.MagicMock()
|
||||
mm.analyse_entry.side_effect = Error('error on analysis', status=900)
|
||||
mm.id = 'magic_mock'
|
||||
mm.analyse_entries.side_effect = Error('error on analysis', status=500)
|
||||
self.senpy.plugins['MOCK'] = mm
|
||||
resp = self.senpy.analyse(input='nothing', algorithm='MOCK')
|
||||
assert resp['message'] == 'error on analysis'
|
||||
assert resp['status'] == 900
|
||||
try:
|
||||
self.senpy.analyse(input='nothing', algorithm='MOCK')
|
||||
assert False
|
||||
except Error as ex:
|
||||
assert ex['message'] == 'error on analysis'
|
||||
assert ex['status'] == 500
|
||||
|
||||
mm.analyse.side_effect = Exception('generic exception on analysis')
|
||||
mm.analyse_entry.side_effect = Exception(
|
||||
mm.analyse_entries.side_effect = Exception(
|
||||
'generic exception on analysis')
|
||||
resp = self.senpy.analyse(input='nothing', algorithm='MOCK')
|
||||
assert resp['message'] == 'generic exception on analysis'
|
||||
assert resp['status'] == 500
|
||||
|
||||
try:
|
||||
self.senpy.analyse(input='nothing', algorithm='MOCK')
|
||||
assert False
|
||||
except Error as ex:
|
||||
assert ex['message'] == 'generic exception on analysis'
|
||||
assert ex['status'] == 500
|
||||
|
||||
def test_filtering(self):
|
||||
""" Filtering plugins """
|
||||
@@ -124,12 +167,13 @@ class ExtensionsTest(TestCase):
|
||||
assert len(senpy.plugins) > 1
|
||||
|
||||
def test_convert_emotions(self):
|
||||
self.senpy.activate_all()
|
||||
plugin = {
|
||||
self.senpy.activate_all(sync=True)
|
||||
plugin = Plugin({
|
||||
'id': 'imaginary',
|
||||
'onyx:usesEmotionModel': 'emoml:fsre-dimensions'
|
||||
}
|
||||
})
|
||||
eSet1 = EmotionSet()
|
||||
eSet1.prov__wasGeneratedBy = plugin['id']
|
||||
eSet1['onyx:hasEmotion'].append(Emotion({
|
||||
'emoml:arousal': 1,
|
||||
'emoml:potency': 0,
|
||||
@@ -145,19 +189,31 @@ class ExtensionsTest(TestCase):
|
||||
'conversion': 'full'}
|
||||
r1 = deepcopy(response)
|
||||
self.senpy.convert_emotions(r1,
|
||||
plugin,
|
||||
[plugin, ],
|
||||
params)
|
||||
assert len(r1.entries[0].emotions) == 2
|
||||
params['conversion'] = 'nested'
|
||||
r2 = deepcopy(response)
|
||||
self.senpy.convert_emotions(r2,
|
||||
plugin,
|
||||
[plugin, ],
|
||||
params)
|
||||
assert len(r2.entries[0].emotions) == 1
|
||||
assert r2.entries[0].emotions[0]['prov:wasDerivedFrom'] == eSet1
|
||||
params['conversion'] = 'filtered'
|
||||
r3 = deepcopy(response)
|
||||
self.senpy.convert_emotions(r3,
|
||||
plugin,
|
||||
[plugin, ],
|
||||
params)
|
||||
assert len(r3.entries[0].emotions) == 1
|
||||
r3.jsonld()
|
||||
|
||||
# def test_async_plugin(self):
|
||||
# """ We should accept multiprocessing plugins with async=False"""
|
||||
# thread1 = self.senpy.activate_plugin("Async", sync=False)
|
||||
# thread1.join(timeout=1)
|
||||
# assert len(self.senpy.plugins['Async'].value) == 4
|
||||
|
||||
# resp = self.senpy.analyse(input='nothing', algorithm='Async')
|
||||
|
||||
# assert len(resp.entries[0].async_values) == 2
|
||||
# self.senpy.activate_plugin("Async", sync=True)
|
||||
|
@@ -11,8 +11,12 @@ from senpy.models import (Emotion,
|
||||
Entry,
|
||||
Error,
|
||||
Results,
|
||||
Sentiment)
|
||||
from senpy.plugins import SenpyPlugin
|
||||
Sentiment,
|
||||
Plugins,
|
||||
Plugin,
|
||||
from_string,
|
||||
from_dict)
|
||||
from senpy import plugins
|
||||
from pprint import pprint
|
||||
|
||||
|
||||
@@ -53,8 +57,8 @@ class ModelsTest(TestCase):
|
||||
assert (received["entries"][0]["nif:isString"] != "Not testing")
|
||||
|
||||
def test_id(self):
|
||||
''' Adding the id after creation should overwrite the automatic ID
|
||||
'''
|
||||
""" Adding the id after creation should overwrite the automatic ID
|
||||
"""
|
||||
r = Entry()
|
||||
j = r.jsonld()
|
||||
assert '@id' in j
|
||||
@@ -94,20 +98,32 @@ class ModelsTest(TestCase):
|
||||
r.validate()
|
||||
|
||||
def test_plugins(self):
|
||||
self.assertRaises(Error, SenpyPlugin)
|
||||
p = SenpyPlugin({"name": "dummy", "version": 0})
|
||||
self.assertRaises(Error, plugins.Plugin)
|
||||
p = plugins.Plugin({"name": "dummy",
|
||||
"version": 0,
|
||||
"extra_params": {
|
||||
"none": {
|
||||
"options": ["es", ],
|
||||
"required": False,
|
||||
"default": "0"
|
||||
}
|
||||
}})
|
||||
c = p.jsonld()
|
||||
assert "info" not in c
|
||||
assert "repo" not in c
|
||||
assert "extra_params" in c
|
||||
logging.debug("Framed:")
|
||||
assert '@type' in c
|
||||
assert c['@type'] == 'plugin'
|
||||
assert 'info' not in c
|
||||
assert 'repo' not in c
|
||||
assert 'extra_params' in c
|
||||
logging.debug('Framed:')
|
||||
logging.debug(c)
|
||||
p.validate()
|
||||
assert 'es' in c['extra_params']['none']['options']
|
||||
assert isinstance(c['extra_params']['none']['options'], list)
|
||||
|
||||
def test_str(self):
|
||||
"""The string representation shouldn't include private variables"""
|
||||
r = Results()
|
||||
p = SenpyPlugin({"name": "STR test", "version": 0})
|
||||
p = plugins.Plugin({"name": "STR test", "version": 0})
|
||||
p._testing = 0
|
||||
s = str(p)
|
||||
assert "_testing" not in s
|
||||
@@ -143,3 +159,40 @@ class ModelsTest(TestCase):
|
||||
print(t)
|
||||
g = rdflib.Graph().parse(data=t, format='turtle')
|
||||
assert len(g) == len(triples)
|
||||
|
||||
def test_plugin_list(self):
|
||||
"""The plugin list should be of type \"plugins\""""
|
||||
plugs = Plugins()
|
||||
c = plugs.jsonld()
|
||||
assert '@type' in c
|
||||
assert c['@type'] == 'plugins'
|
||||
|
||||
def test_single_plugin(self):
|
||||
"""A response with a single plugin should still return a list"""
|
||||
plugs = Plugins()
|
||||
p = Plugin({'id': str(1),
|
||||
'version': 0,
|
||||
'description': 'dummy'})
|
||||
plugs.plugins.append(p)
|
||||
assert isinstance(plugs.plugins, list)
|
||||
js = plugs.jsonld()
|
||||
assert isinstance(js['plugins'], list)
|
||||
|
||||
def test_from_string(self):
|
||||
results = {
|
||||
'@type': 'results',
|
||||
'@id': 'prueba',
|
||||
'entries': [{
|
||||
'@id': 'entry1',
|
||||
'@type': 'entry',
|
||||
'text': 'TEST'
|
||||
}]
|
||||
}
|
||||
recovered = from_dict(results)
|
||||
assert isinstance(recovered, Results)
|
||||
assert isinstance(recovered.entries[0], Entry)
|
||||
|
||||
string = json.dumps(results)
|
||||
recovered = from_string(string)
|
||||
assert isinstance(recovered, Results)
|
||||
assert isinstance(recovered.entries[0], Entry)
|
||||
|
@@ -83,7 +83,39 @@ class PluginsTest(TestCase):
|
||||
res2 = a.analyse(input=1)
|
||||
assert res2.entries[0].nif__isString == 2
|
||||
|
||||
def test_two(self):
|
||||
def test_corrupt_shelf(self):
|
||||
''' Reusing the values of a previous shelf '''
|
||||
|
||||
emptyfile = os.path.join(self.shelf_dir, "emptyfile")
|
||||
invalidfile = os.path.join(self.shelf_dir, "invalid_file")
|
||||
with open(emptyfile, 'w+b'), open(invalidfile, 'w+b') as inf:
|
||||
inf.write(b'ohno')
|
||||
|
||||
files = {emptyfile: ['empty file', (EOFError, IndexError)],
|
||||
invalidfile: ['invalid file', (pickle.UnpicklingError, IndexError)]}
|
||||
|
||||
for fn in files:
|
||||
with open(fn, 'rb') as f:
|
||||
msg, error = files[fn]
|
||||
a = ShelfDummyPlugin(info={
|
||||
'name': 'shelve',
|
||||
'version': 'test',
|
||||
'shelf_file': f.name
|
||||
})
|
||||
assert os.path.isfile(a.shelf_file)
|
||||
print('Shelf file: %s' % a.shelf_file)
|
||||
with self.assertRaises(error):
|
||||
a.sh['a'] = 'fromA'
|
||||
a.save()
|
||||
del a._sh
|
||||
assert os.path.isfile(a.shelf_file)
|
||||
a.force_shelf = True
|
||||
a.sh['a'] = 'fromA'
|
||||
a.save()
|
||||
b = pickle.load(f)
|
||||
assert b['a'] == 'fromA'
|
||||
|
||||
def test_reuse_shelf(self):
|
||||
''' Reusing the values of a previous shelf '''
|
||||
a = ShelfDummyPlugin(info={
|
||||
'name': 'shelve',
|
||||
|
Reference in New Issue
Block a user