mirror of
https://github.com/gsi-upm/senpy
synced 2025-10-19 17:58:28 +00:00
Compare commits
39 Commits
0.9.2
...
44-add-bas
Author | SHA1 | Date | |
---|---|---|---|
|
d8dead1908 | ||
|
87dcdb9fbc | ||
|
67ef4b60bd | ||
|
da4b11e5b5 | ||
|
c0aa7ddc3c | ||
|
5e2ada1654 | ||
|
7a188586c5 | ||
|
b768b215c5 | ||
|
d1f1b9a15a | ||
|
52a0f3f4c8 | ||
|
55c32dcd7c | ||
|
0093bc34d5 | ||
|
67bae9a20d | ||
|
551a5cb176 | ||
|
d6f4cc2dd2 | ||
|
4af692091a | ||
|
ec68ff0b90 | ||
|
738da490db | ||
|
d29c42fd2e | ||
|
23c88d0acc | ||
|
dcaaa591b7 | ||
|
15ab5f4c25 | ||
|
92189822d8 | ||
|
fbb418c365 | ||
|
081078ddd6 | ||
|
7c8dbf3262 | ||
|
41dc89b23b | ||
|
a951696317 | ||
|
1087692de2 | ||
|
3e2b8baeb2 | ||
|
21a5a3f201 | ||
|
abd401f863 | ||
|
bfc588a915 | ||
|
f93eed2cf5 | ||
|
0204e0b8e9 | ||
|
701f46b9f1 | ||
|
d1eca04eeb | ||
|
df7efbc57d | ||
|
89f3a0eca9 |
@@ -55,6 +55,7 @@ push-latest:
|
||||
only:
|
||||
- master
|
||||
- triggers
|
||||
- fix-makefiles
|
||||
|
||||
push-github:
|
||||
stage: deploy
|
||||
|
@@ -2,18 +2,16 @@ export
|
||||
NAME ?= $(shell basename $(CURDIR))
|
||||
VERSION ?= $(shell git describe --tags --dirty 2>/dev/null)
|
||||
|
||||
ifeq ($(VERSION),)
|
||||
VERSION:=unknown
|
||||
endif
|
||||
|
||||
# Get the location of this makefile.
|
||||
MK_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
|
||||
-include .env
|
||||
-include ../.env
|
||||
|
||||
.FORCE:
|
||||
|
||||
version: .FORCE
|
||||
@echo $(VERSION) > $(NAME)/VERSION
|
||||
@echo $(VERSION)
|
||||
|
||||
help: ## Show this help.
|
||||
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/\(.*:\)[^#]*##\s*\(.*\)/\1\t\2/' | column -t -s " "
|
||||
|
||||
@@ -35,4 +33,4 @@ include $(MK_DIR)/git.mk
|
||||
info:: ## List all variables
|
||||
env
|
||||
|
||||
.PHONY:: config help ci version .FORCE
|
||||
.PHONY:: config help ci
|
||||
|
@@ -1,3 +1,4 @@
|
||||
IMAGENAME?=$(NAME)
|
||||
IMAGEWTAG?=$(IMAGENAME):$(VERSION)
|
||||
|
||||
docker-login: ## Log in to the registry. It will only be used in the server, or when running a CI task locally (if CI_BUILD_TOKEN is set).
|
||||
@@ -22,4 +23,7 @@ login:: docker-login
|
||||
|
||||
clean:: docker-clean
|
||||
|
||||
docker-info:
|
||||
@echo IMAGEWTAG=${IMAGEWTAG}
|
||||
|
||||
.PHONY:: docker-login docker-clean login clean
|
||||
|
@@ -1,9 +1,17 @@
|
||||
PYVERSIONS ?= 2.7
|
||||
PYVERSIONS ?= 3.5
|
||||
PYMAIN ?= $(firstword $(PYVERSIONS))
|
||||
TARNAME ?= $(NAME)-$(VERSION).tar.gz
|
||||
VERSIONFILE ?= $(NAME)/VERSION
|
||||
|
||||
DEVPORT ?= 6000
|
||||
|
||||
|
||||
.FORCE:
|
||||
|
||||
version: .FORCE
|
||||
@echo $(VERSION) > $(VERSIONFILE)
|
||||
@echo $(VERSION)
|
||||
|
||||
yapf: ## Format python code
|
||||
yapf -i -r $(NAME)
|
||||
yapf -i -r tests
|
||||
@@ -20,7 +28,7 @@ quick_build: $(addprefix build-, $(PYMAIN))
|
||||
build: $(addprefix build-, $(PYVERSIONS)) ## Build all images / python versions
|
||||
|
||||
build-%: version Dockerfile-% ## Build a specific version (e.g. build-2.7)
|
||||
docker build -t '$(IMAGEWTAG)-python$*' --cache-from $(IMAGENAME):python$* -f Dockerfile-$* .;
|
||||
docker build -t '$(IMAGEWTAG)-python$*' -f Dockerfile-$* .;
|
||||
|
||||
dev-%: ## Launch a specific development environment using docker (e.g. dev-2.7)
|
||||
@docker start $(NAME)-dev$* || (\
|
||||
@@ -34,10 +42,10 @@ dev: dev-$(PYMAIN) ## Launch a development environment using docker, using the d
|
||||
|
||||
quick_test: test-$(PYMAIN)
|
||||
|
||||
test-%: ## Run setup.py from in an isolated container, built from the base image. (e.g. test-2.7)
|
||||
test-%: build-% ## Run setup.py from in an isolated container, built from the base image. (e.g. test-2.7)
|
||||
# This speeds tests up because the image has most (if not all) of the dependencies already.
|
||||
docker rm $(NAME)-test-$* || true
|
||||
docker create -ti --name $(NAME)-test-$* --entrypoint="" -w /usr/src/app/ $(IMAGENAME):python$* python setup.py test
|
||||
docker create -ti --name $(NAME)-test-$* --entrypoint="" -w /usr/src/app/ $(IMAGEWTAG)-python$* python setup.py test
|
||||
docker cp . $(NAME)-test-$*:/usr/src/app
|
||||
docker start -a $(NAME)-test-$*
|
||||
|
||||
@@ -89,4 +97,4 @@ clean:: ## Clean older docker images and containers related to this project and
|
||||
@docker ps -a | grep $(IMAGENAME) | awk '{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1;}}' | xargs docker rm -v 2>/dev/null|| true
|
||||
@docker images | grep $(IMAGENAME) | awk '{ split($$2, vers, "-"); if(vers[0] != "${VERSION}"){ print $$1":"$$2;}}' | xargs docker rmi 2>/dev/null|| true
|
||||
|
||||
.PHONY:: yapf dockerfiles Dockerfile-% quick_build build build-% dev-% quick-dev test quick_test push-latest push-latest-% push-% push
|
||||
.PHONY:: yapf dockerfiles Dockerfile-% quick_build build build-% dev-% quick-dev test quick_test push-latest push-latest-% push-% push version .FORCE
|
||||
|
@@ -2,6 +2,12 @@ from python:{{PYVERSION}}
|
||||
|
||||
MAINTAINER J. Fernando Sánchez <jf.sanchez@upm.es>
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libblas-dev liblapack-dev liblapacke-dev gfortran \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip install --no-cache-dir --upgrade numpy scipy scikit-learn
|
||||
|
||||
RUN mkdir /cache/ /senpy-plugins /data/
|
||||
|
||||
VOLUME /data/
|
||||
@@ -14,9 +20,9 @@ ONBUILD WORKDIR /senpy-plugins/
|
||||
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY test-requirements.txt requirements.txt /usr/src/app/
|
||||
RUN pip install --use-wheel -r test-requirements.txt -r requirements.txt
|
||||
COPY test-requirements.txt requirements.txt extra-requirements.txt /usr/src/app/
|
||||
RUN pip install --no-cache-dir -r test-requirements.txt -r requirements.txt -r extra-requirements.txt
|
||||
COPY . /usr/src/app/
|
||||
RUN pip install --no-index --no-deps --editable .
|
||||
RUN pip install --no-cache-dir --no-index --no-deps --editable .
|
||||
|
||||
ENTRYPOINT ["python", "-m", "senpy", "-f", "/senpy-plugins/", "--host", "0.0.0.0"]
|
||||
|
106
docs/SenpyClientUse.rst
Normal file
106
docs/SenpyClientUse.rst
Normal file
@@ -0,0 +1,106 @@
|
||||
|
||||
Client
|
||||
======
|
||||
|
||||
Demo Endpoint
|
||||
-------------
|
||||
|
||||
Import Client and send a request
|
||||
|
||||
.. code:: python
|
||||
|
||||
from senpy.client import Client
|
||||
|
||||
c = Client('http://latest.senpy.cluster.gsi.dit.upm.es/api')
|
||||
r = c.analyse('I like Pizza', algorithm='sentiment140')
|
||||
|
||||
Print response
|
||||
|
||||
.. code:: python
|
||||
|
||||
for entry in r.entries:
|
||||
print('{} -> {}'.format(entry['text'], entry['sentiments'][0]['marl:hasPolarity']))
|
||||
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
I like Pizza -> marl:Positive
|
||||
|
||||
|
||||
Obtain a list of available plugins
|
||||
|
||||
.. code:: python
|
||||
|
||||
for plugin in c.request('/plugins')['plugins']:
|
||||
print(plugin['name'])
|
||||
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
emoRand
|
||||
rand
|
||||
sentiment140
|
||||
|
||||
|
||||
Local Endpoint
|
||||
--------------
|
||||
|
||||
Run a docker container with Senpy image and default plugins
|
||||
|
||||
.. code::
|
||||
|
||||
docker run -ti --name 'SenpyEndpoint' -d -p 5000:5000 gsiupm/senpy:0.8.6 --host 0.0.0.0 --default-plugins
|
||||
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
a0157cd98057072388bfebeed78a830da7cf0a796f4f1a3fd9188f9f2e5fe562
|
||||
|
||||
|
||||
Import client and send a request to localhost
|
||||
|
||||
.. code:: python
|
||||
|
||||
c_local = Client('http://127.0.0.1:5000/api')
|
||||
r = c_local.analyse('Hello world', algorithm='sentiment140')
|
||||
|
||||
Print response
|
||||
|
||||
.. code:: python
|
||||
|
||||
for entry in r.entries:
|
||||
print('{} -> {}'.format(entry['text'], entry['sentiments'][0]['marl:hasPolarity']))
|
||||
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
Hello world -> marl:Neutral
|
||||
|
||||
|
||||
Obtain a list of available plugins deployed locally
|
||||
|
||||
.. code:: python
|
||||
|
||||
c_local.plugins().keys()
|
||||
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
rand
|
||||
sentiment140
|
||||
emoRand
|
||||
|
||||
|
||||
Stop the docker container
|
||||
|
||||
.. code:: python
|
||||
|
||||
!docker stop SenpyEndpoint
|
||||
!docker rm SenpyEndpoint
|
||||
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
SenpyEndpoint
|
||||
SenpyEndpoint
|
||||
|
113
docs/plugins-definition.rst
Normal file
113
docs/plugins-definition.rst
Normal file
@@ -0,0 +1,113 @@
|
||||
Advanced plugin definition
|
||||
--------------------------
|
||||
In addition to finding plugins defined in source code files, senpy can also load a special type of definition file (`.senpy` files).
|
||||
This used to be the only mechanism for loading in earlier versions of senpy.
|
||||
|
||||
The definition file contains basic information
|
||||
|
||||
Lastly, it is also possible to add new plugins programmatically.
|
||||
|
||||
.. contents:: :local:
|
||||
|
||||
What is a plugin?
|
||||
=================
|
||||
|
||||
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.
|
||||
|
||||
Definition files
|
||||
================
|
||||
|
||||
The definition file complements and overrides the attributes provided by the plugin.
|
||||
It 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: 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
|
||||
|
||||
extra_params:
|
||||
hello_param:
|
||||
aliases: # required
|
||||
- hello_param
|
||||
- hello
|
||||
required: true
|
||||
default: Hi you
|
||||
values:
|
||||
- Hi you
|
||||
- Hello y'all
|
||||
- Howdy
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
|
||||
Example plugin with a definition file
|
||||
=====================================
|
||||
|
||||
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
|
||||
description: Hello World
|
||||
|
||||
Now, in a file named ``helloworld.py``:
|
||||
|
||||
.. code:: python
|
||||
|
||||
#!/bin/env python
|
||||
#helloworld.py
|
||||
|
||||
from senpy import AnalysisPlugin
|
||||
from senpy import Sentiment
|
||||
|
||||
|
||||
class HelloWorld(AnalysisPlugin):
|
||||
|
||||
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
|
||||
|
||||
The complete code of the example plugin is available `here <https://lab.cluster.gsi.dit.upm.es/senpy/plugin-prueba>`__.
|
313
docs/plugins.rst
313
docs/plugins.rst
@@ -1,6 +1,8 @@
|
||||
Developing new plugins
|
||||
----------------------
|
||||
This document describes how to develop a new analysis plugin. For an example of conversion plugins, see :doc:`conversion`.
|
||||
This document contains the minimum to get you started with developing new analysis plugin.
|
||||
For an example of conversion plugins, see :doc:`conversion`.
|
||||
For a description of definition files, see :doc:`plugins-definition`.
|
||||
|
||||
A more step-by-step tutorial with slides is available `here <https://lab.cluster.gsi.dit.upm.es/senpy/senpy-tutorial>`__
|
||||
|
||||
@@ -9,83 +11,29 @@ A more step-by-step tutorial with slides is available `here <https://lab.cluster
|
||||
What is a plugin?
|
||||
=================
|
||||
|
||||
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.
|
||||
|
||||
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: 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
|
||||
|
||||
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 plugin is a python object that can process entries. Given an entry, it will modify it, add annotations to it, or generate new entries.
|
||||
|
||||
|
||||
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 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
|
||||
=======
|
||||
What is an entry?
|
||||
=================
|
||||
|
||||
Entries are objects that can be annotated.
|
||||
In general, they will be a piece of text.
|
||||
By default, entries are `NIF contexts <http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core/nif-core.html>`_ represented in JSON-LD format.
|
||||
It is a dictionary/JSON object that looks like this:
|
||||
|
||||
.. code:: python
|
||||
|
||||
{
|
||||
"@id": "<unique identifier or blank node name>",
|
||||
"nif:isString": "input text",
|
||||
"sentiments": [ {
|
||||
...
|
||||
}
|
||||
],
|
||||
...
|
||||
}
|
||||
|
||||
Annotations are added to the object like this:
|
||||
|
||||
.. code:: python
|
||||
@@ -100,96 +48,111 @@ 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
|
||||
==============
|
||||
What are annotations?
|
||||
=====================
|
||||
They are objects just like entries.
|
||||
Senpy ships with several default annotations, including: ``Sentiment``, ``Emotion``, ``EmotionSet``...jk bb
|
||||
|
||||
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:
|
||||
What's a plugin made of?
|
||||
========================
|
||||
|
||||
.. code:: yaml
|
||||
When receiving a query, senpy selects what plugin or plugins should process each entry, and in what order.
|
||||
It also makes sure the every entry and the parameters provided by the user meet the plugin requirements.
|
||||
|
||||
name: helloworld
|
||||
module: helloworld
|
||||
version: 0.0
|
||||
threshold: 10
|
||||
description: Hello World
|
||||
Hence, two parts are necessary: 1) the code that will process the entry, and 2) some attributes and metadata that will tell senpy how to interact with the plugin.
|
||||
|
||||
Now, in a file named ``helloworld.py``:
|
||||
In practice, this is what a plugin looks like, tests included:
|
||||
|
||||
|
||||
.. literalinclude:: ../senpy/plugins/example/rand_plugin.py
|
||||
:emphasize-lines: 5-11
|
||||
:language: python
|
||||
|
||||
|
||||
The lines highlighted contain some information about the plugin.
|
||||
In particular, the following information is mandatory:
|
||||
|
||||
* A unique name for the class. In our example, Rand.
|
||||
* The subclass/type of plugin. This is typically either `SentimentPlugin` or `EmotionPlugin`. However, new types of plugin can be created for different annotations. The only requirement is that these new types inherit from `senpy.Analysis`
|
||||
* A description of the plugin. This can be done simply by adding a doc to the class.
|
||||
* A version, which should get updated.
|
||||
* An author name.
|
||||
|
||||
|
||||
Plugins Code
|
||||
============
|
||||
|
||||
The basic methods in a plugin are:
|
||||
|
||||
* 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.
|
||||
* activate: used to load memory-hungry resources. For instance, to train a classifier.
|
||||
* deactivate: used to free up resources when the plugin is no longer needed.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
How does senpy find modules?
|
||||
============================
|
||||
|
||||
Senpy looks for files of two types:
|
||||
|
||||
* Python files of the form `senpy_<NAME>.py` or `<NAME>_plugin.py`. In these files, it will look for: 1) Instances that inherit from `senpy.Plugin`, or subclasses of `senpy.Plugin` that can be initialized without a configuration file. i.e. classes that contain all the required attributes for a plugin.
|
||||
* Plugin definition files (see :doc:`advanced-plugins`)
|
||||
|
||||
Defining additional parameters
|
||||
==============================
|
||||
|
||||
Your plugin may ask for additional parameters from the users of the service by using the attribute ``extra_params`` in your plugin definition.
|
||||
It takes a dictionary, where the keys are the name of the argument/parameter, and the value has the following fields:
|
||||
|
||||
* aliases: the different names which can be used in the request to use the parameter.
|
||||
* required: if set to true, users need to provide this parameter unless a default is set.
|
||||
* options: the different acceptable values of the parameter (i.e. an enum). If set, the value provided must match one of the options.
|
||||
* default: the default value of the parameter, if none is provided in the request.
|
||||
|
||||
.. code:: python
|
||||
|
||||
#!/bin/env python
|
||||
#helloworld.py
|
||||
|
||||
from senpy.plugins import AnalysisPlugin
|
||||
from senpy.models import Sentiment
|
||||
"extra_params":{
|
||||
"language": {
|
||||
"aliases": ["language", "lang", "l"],
|
||||
"required": True,
|
||||
"options": ["es", "en"],
|
||||
"default": "es"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class HelloWorld(AnalysisPlugin):
|
||||
|
||||
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
|
||||
|
||||
The complete code of the example plugin is available `here <https://lab.cluster.gsi.dit.upm.es/senpy/plugin-prueba>`__.
|
||||
|
||||
Loading data and files
|
||||
======================
|
||||
|
||||
Most plugins will need access to files (dictionaries, lexicons, etc.).
|
||||
It is good practice to specify the paths of these files in the plugin configuration, so the same code can be reused with different resources.
|
||||
These files are usually heavy or under a license that does not allow redistribution.
|
||||
For this reason, senpy has a `data_folder` that is separated from the source files.
|
||||
The location of this folder is controlled programmatically or by setting the `SENPY_DATA` environment variable.
|
||||
|
||||
Plugins have a convenience function `self.open` which will automatically prepend the data folder to relative paths:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
name: dictworld
|
||||
module: dictworld
|
||||
dictionary_path: <PATH OF THE FILE>
|
||||
|
||||
The path can be either absolute, or relative.
|
||||
|
||||
From absolute paths
|
||||
???????????????????
|
||||
|
||||
Absolute paths (such as ``/data/dictionary.csv`` are straightfoward:
|
||||
|
||||
.. code:: python
|
||||
|
||||
with open(os.path.join(self.dictionary_path) as f:
|
||||
...
|
||||
|
||||
From relative paths
|
||||
???????????????????
|
||||
Since plugins are loading dynamically, relative paths will refer to the current working directory.
|
||||
Instead, what you usually want is to load files *relative to the plugin source folder*, like so:
|
||||
|
||||
|
||||
::
|
||||
|
||||
.
|
||||
..
|
||||
plugin.senpy
|
||||
plugin.py
|
||||
dictionary.csv
|
||||
|
||||
For this, we need to first get the path of your source folder first, like so:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import os
|
||||
root = os.path.realpath(__file__)
|
||||
with open(os.path.join(root, self.dictionary_path) as f:
|
||||
...
|
||||
|
||||
|
||||
class PluginWithResources(AnalysisPlugin):
|
||||
file_in_data = <FILE PATH>
|
||||
file_in_sources = <FILE PATH>
|
||||
|
||||
def activate(self):
|
||||
with self.open(self.file_in_data) as f:
|
||||
self._classifier = train_from_file(f)
|
||||
file_in_source = os.path.join(self.get_folder(), self.file_in_sources)
|
||||
with self.open(file_in_source) as f:
|
||||
pass
|
||||
|
||||
|
||||
It is good practice to specify the paths of these files in the plugin configuration, so the same code can be reused with different resources.
|
||||
|
||||
|
||||
Docker image
|
||||
@@ -199,7 +162,16 @@ Add the following dockerfile to your project to generate a docker image with you
|
||||
|
||||
.. code:: dockerfile
|
||||
|
||||
FROM gsiupm/senpy:0.8.8
|
||||
FROM gsiupm/senpy
|
||||
|
||||
Once you make sure your plugin works with a specific version of senpy, modify that file to make sure your build will work even if senpy gets updated.
|
||||
e.g.:
|
||||
|
||||
|
||||
.. code:: dockerfile
|
||||
|
||||
FROM gsiupm/senpy:1.0.1
|
||||
|
||||
|
||||
This will copy your source folder to the image, and install all dependencies.
|
||||
Now, to build an image:
|
||||
@@ -215,7 +187,7 @@ And you can run it with:
|
||||
docker run -p 5000:5000 gsiupm/exampleplugin
|
||||
|
||||
|
||||
If the plugin non-source files (:ref:`loading data and files`), the recommended way is to use absolute paths.
|
||||
If the plugin uses non-source files (:ref:`loading data and files`), the recommended way is to use `SENPY_DATA` folder.
|
||||
Data can then be mounted in the container or added to the image.
|
||||
The former is recommended for open source plugins with licensed resources, whereas the latter is the most convenient and can be used for private images.
|
||||
|
||||
@@ -229,7 +201,7 @@ Adding data to the image:
|
||||
|
||||
.. code:: dockerfile
|
||||
|
||||
FROM gsiupm/senpy:0.8.8
|
||||
FROM gsiupm/senpy:1.0.1
|
||||
COPY data /
|
||||
|
||||
F.A.Q.
|
||||
@@ -245,7 +217,7 @@ 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.
|
||||
For instance, a chunker may split one entry into several.
|
||||
On the other hand, a conversion plugin may leave out those entries that do not contain relevant information.
|
||||
|
||||
|
||||
@@ -275,11 +247,13 @@ Training a classifier can be time time consuming. To avoid running the training
|
||||
def deactivate(self):
|
||||
self.close()
|
||||
|
||||
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.
|
||||
|
||||
By default the ShelfMixin creates a file based on the plugin name and stores it in that plugin's folder.
|
||||
However, you can manually specify a 'shelf_file' in your .senpy file.
|
||||
|
||||
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.
|
||||
If you do not care about the data in the shelf, you can force your plugin to remove the corrupted file and load anyway, set the 'force_shelf' to True in your plugin and start it again.
|
||||
|
||||
How can I turn an external service into a plugin?
|
||||
?????????????????????????????????????????????????
|
||||
@@ -313,50 +287,11 @@ This example ilustrate how to implement a plugin that accesses the Sentiment140
|
||||
prefix=p,
|
||||
marl__hasPolarity=polarity,
|
||||
marl__polarityValue=polarity_value)
|
||||
sentiment.prov__wasGeneratedBy = self.id
|
||||
sentiment.prov(self)
|
||||
entry.sentiments.append(sentiment)
|
||||
yield entry
|
||||
|
||||
|
||||
Can my plugin require additional parameters from the user?
|
||||
??????????????????????????????????????????????????????????
|
||||
|
||||
You can add extra parameters in the definition file under the attribute ``extra_params``.
|
||||
It takes a dictionary, where the keys are the name of the argument/parameter, and the value has the following fields:
|
||||
|
||||
* aliases: the different names which can be used in the request to use the parameter.
|
||||
* required: if set to true, users need to provide this parameter unless a default is set.
|
||||
* options: the different acceptable values of the parameter (i.e. an enum). If set, the value provided must match one of the options.
|
||||
* default: the default value of the parameter, if none is provided in the request.
|
||||
|
||||
.. code:: python
|
||||
|
||||
extra_params
|
||||
language:
|
||||
aliases:
|
||||
- language
|
||||
- lang
|
||||
- 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?
|
||||
???????????????????????????????????????????
|
||||
|
||||
@@ -371,7 +306,7 @@ Additionally, with the ``--pdb`` option you will be dropped into a pdb post mort
|
||||
|
||||
.. code:: bash
|
||||
|
||||
senpy --pdb
|
||||
python -m pdb yourplugin.py
|
||||
|
||||
Where can I find more code examples?
|
||||
????????????????????????????????????
|
||||
|
@@ -1,8 +1,11 @@
|
||||
What is Senpy?
|
||||
--------------
|
||||
|
||||
Web services can get really complex: data validation, user interaction, formatting, logging., etc.
|
||||
The figure below summarizes the typical features in an analysis service.
|
||||
Senpy is a framework for text analysis using Linked Data. There are three main applications of Senpy so far: sentiment and emotion analysis, user profiling and entity recoginition. Annotations and Services are compliant with NIF (NLP Interchange Format).
|
||||
|
||||
Senpy aims at providing a framework where analysis modules can be integrated easily as plugins, and providing a core functionality for managing tasks such as data validation, user interaction, formatting, logging, translation to linked data, etc.
|
||||
|
||||
The figure below summarizes the typical features in a text analysis service.
|
||||
Senpy implements all the common blocks, so developers can focus on what really matters: great analysis algorithms that solve real problems.
|
||||
|
||||
.. image:: senpy-framework.png
|
||||
|
@@ -7,7 +7,8 @@ The senpy server is launched via the `senpy` command:
|
||||
|
||||
usage: senpy [-h] [--level logging_level] [--debug] [--default-plugins]
|
||||
[--host HOST] [--port PORT] [--plugins-folder PLUGINS_FOLDER]
|
||||
[--only-install]
|
||||
[--only-install] [--only-list] [--data-folder DATA_FOLDER]
|
||||
[--threaded] [--version]
|
||||
|
||||
Run a Senpy server
|
||||
|
||||
@@ -22,6 +23,13 @@ The senpy server is launched via the `senpy` command:
|
||||
--plugins-folder PLUGINS_FOLDER, -f PLUGINS_FOLDER
|
||||
Where to look for plugins.
|
||||
--only-install, -i Do not run a server, only install plugin dependencies
|
||||
--only-list, --list Do not run a server, only list plugins found
|
||||
--data-folder DATA_FOLDER, --data DATA_FOLDER
|
||||
Where to look for data. It be set with the SENPY_DATA
|
||||
environment variable as well.
|
||||
--threaded Run a threaded server
|
||||
--version, -v Output the senpy version and exit
|
||||
|
||||
|
||||
|
||||
When launched, the server will recursively look for plugins in the specified plugins folder (the current working directory by default).
|
||||
|
@@ -1,8 +1,24 @@
|
||||
Vocabularies and model
|
||||
======================
|
||||
|
||||
The model used in Senpy is based on the following vocabularies:
|
||||
The model used in Senpy is based on NIF 2.0 [1], which defines a semantic format and API for improving interoperability among natural language processing services.
|
||||
|
||||
* 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 has been applied to sentiment and emotion analysis services using the following vocabularies:
|
||||
|
||||
* Marl [2,6], a vocabulary designed to annotate and describe subjetive opinions expressed on the web or in information systems.
|
||||
* Onyx [3,5], which is built one the same principles as Marl to annotate and describe emotions, and provides interoperability with Emotion Markup Language.
|
||||
|
||||
An overview of the vocabularies and their use can be found in [4].
|
||||
|
||||
|
||||
[1] Guidelines for developing NIF-based NLP services, Final Community Group Report 22 December 2015 Available at: https://www.w3.org/2015/09/bpmlod-reports/nif-based-nlp-webservices/
|
||||
|
||||
[2] Marl Ontology Specification, available at http://www.gsi.dit.upm.es/ontologies/marl/
|
||||
|
||||
[3] Onyx Ontology Specification, available at http://www.gsi.dit.upm.es/ontologies/onyx/
|
||||
|
||||
[4] Iglesias, C. A., Sánchez-Rada, J. F., Vulcu, G., & Buitelaar, P. (2017). Linked Data Models for Sentiment and Emotion Analysis in Social Networks. In Sentiment Analysis in Social Networks (pp. 49-69).
|
||||
|
||||
[5] Sánchez-Rada, J. F., & Iglesias, C. A. (2016). Onyx: A linked data approach to emotion representation. Information Processing & Management, 52(1), 99-114.
|
||||
|
||||
[6] Westerski, A., Iglesias Fernandez, C. A., & Tapia Rico, F. (2011). Linked opinions: Describing sentiments on the structured web of data.
|
||||
|
23
example-plugins/README.md
Normal file
23
example-plugins/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
This is a collection of plugins that exemplify certain aspects of plugin development with senpy.
|
||||
|
||||
The first series of plugins the `basic` ones.
|
||||
Their starting point is a classification function defined in `basic.py`.
|
||||
They all include testing and running them as a script will run all tests.
|
||||
In ascending order of customization, the plugins are:
|
||||
|
||||
* Basic is the simplest plugin of all. It leverages the `SentimentBox` Plugin class to create a plugin out of a classification method, and `MappingMixin` to convert the labels from (`pos`, `neg`) to (`marl:Positive`, `marl:Negative`
|
||||
* Basic_box is just like the previous one, but replaces the mixin with a custom function.
|
||||
* Basic_configurable is a version of `basic` with a configurable map of emojis for each sentiment.
|
||||
* Basic_parameterized like `basic_info`, but users set the map in each query (via `extra_parameters`).
|
||||
* Basic_analyse\_entry uses the more general `analyse_entry` method and adds the annotations individually.
|
||||
|
||||
|
||||
In rest of the plugins show advanced topics:
|
||||
|
||||
* mynoop: shows how to add a definition file with external requirements for a plugin. Doing this with a python-only module would require moving all imports of the requirements to their functions, which is considered bad practice.
|
||||
* Async: a barebones example of training a plugin and analyzing data in parallel.
|
||||
|
||||
All of the plugins in this folder include a set of test cases and they are periodically tested with the latest version of senpy.
|
||||
|
||||
Additioanlly, for an example of stand-alone plugin that can be tested and deployed with docker, take a look at: lab.cluster.gsi.dit.upm.es/senpy/plugin-example
|
||||
bbm
|
@@ -1,4 +1,4 @@
|
||||
from senpy.plugins import AnalysisPlugin
|
||||
from senpy import AnalysisPlugin
|
||||
|
||||
import multiprocessing
|
||||
|
||||
@@ -7,10 +7,15 @@ def _train(process_number):
|
||||
return process_number
|
||||
|
||||
|
||||
class AsyncPlugin(AnalysisPlugin):
|
||||
class Async(AnalysisPlugin):
|
||||
'''An example of an asynchronous module'''
|
||||
author = '@balkian'
|
||||
version = '0.2'
|
||||
async = True
|
||||
|
||||
def _do_async(self, num_processes):
|
||||
pool = multiprocessing.Pool(processes=num_processes)
|
||||
values = pool.map(_train, range(num_processes))
|
||||
values = sorted(pool.map(_train, range(num_processes)))
|
||||
|
||||
return values
|
||||
|
||||
@@ -22,5 +27,11 @@ class AsyncPlugin(AnalysisPlugin):
|
||||
entry.async_values = values
|
||||
yield entry
|
||||
|
||||
def test(self):
|
||||
pass
|
||||
test_cases = [
|
||||
{
|
||||
'input': 'any',
|
||||
'expected': {
|
||||
'async_values': [0, 1]
|
||||
}
|
||||
}
|
||||
]
|
23
example-plugins/basic.py
Normal file
23
example-plugins/basic.py
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/local/bin/python
|
||||
# coding: utf-8
|
||||
|
||||
emoticons = {
|
||||
'pos': [':)', ':]', '=)', ':D'],
|
||||
'neg': [':(', ':[', '=(']
|
||||
}
|
||||
|
||||
emojis = {
|
||||
'pos': ['😁', '😂', '😃', '😄', '😆', '😅', '😄' '😍'],
|
||||
'neg': ['😢', '😡', '😠', '😞', '😖', '😔', '😓', '😒']
|
||||
}
|
||||
|
||||
|
||||
def get_polarity(text, dictionaries=[emoticons, emojis]):
|
||||
polarity = 'marl:Neutral'
|
||||
for dictionary in dictionaries:
|
||||
for label, values in dictionary.items():
|
||||
for emoticon in values:
|
||||
if emoticon and emoticon in text:
|
||||
polarity = label
|
||||
break
|
||||
return polarity
|
47
example-plugins/basic_analyse_entry_plugin.py
Normal file
47
example-plugins/basic_analyse_entry_plugin.py
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/local/bin/python
|
||||
# coding: utf-8
|
||||
|
||||
from senpy import easy_test, models, plugins
|
||||
|
||||
import basic
|
||||
|
||||
|
||||
class BasicAnalyseEntry(plugins.SentimentPlugin):
|
||||
'''Equivalent to Basic, implementing the analyse_entry method'''
|
||||
|
||||
author = '@balkian'
|
||||
version = '0.1'
|
||||
|
||||
mappings = {
|
||||
'pos': 'marl:Positive',
|
||||
'neg': 'marl:Negative',
|
||||
'default': 'marl:Neutral'
|
||||
}
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
polarity = basic.get_polarity(entry.text)
|
||||
|
||||
polarity = self.mappings.get(polarity, self.mappings['default'])
|
||||
|
||||
s = models.Sentiment(marl__hasPolarity=polarity)
|
||||
s.prov(self)
|
||||
entry.sentiments.append(s)
|
||||
yield entry
|
||||
|
||||
test_cases = [{
|
||||
'input': 'Hello :)',
|
||||
'polarity': 'marl:Positive'
|
||||
}, {
|
||||
'input': 'So sad :(',
|
||||
'polarity': 'marl:Negative'
|
||||
}, {
|
||||
'input': 'Yay! Emojis 😁',
|
||||
'polarity': 'marl:Positive'
|
||||
}, {
|
||||
'input': 'But no emoticons 😢',
|
||||
'polarity': 'marl:Negative'
|
||||
}]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
easy_test()
|
41
example-plugins/basic_box_plugin.py
Normal file
41
example-plugins/basic_box_plugin.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/local/bin/python
|
||||
# coding: utf-8
|
||||
|
||||
from senpy import easy_test, SentimentBox
|
||||
|
||||
import basic
|
||||
|
||||
|
||||
class BasicBox(SentimentBox):
|
||||
''' A modified version of Basic that also does converts annotations manually'''
|
||||
|
||||
author = '@balkian'
|
||||
version = '0.1'
|
||||
|
||||
mappings = {
|
||||
'pos': 'marl:Positive',
|
||||
'neg': 'marl:Negative',
|
||||
'default': 'marl:Neutral'
|
||||
}
|
||||
|
||||
def predict_one(self, input):
|
||||
output = basic.get_polarity(input)
|
||||
return self.mappings.get(output, self.mappings['default'])
|
||||
|
||||
test_cases = [{
|
||||
'input': 'Hello :)',
|
||||
'polarity': 'marl:Positive'
|
||||
}, {
|
||||
'input': 'So sad :(',
|
||||
'polarity': 'marl:Negative'
|
||||
}, {
|
||||
'input': 'Yay! Emojis 😁',
|
||||
'polarity': 'marl:Positive'
|
||||
}, {
|
||||
'input': 'But no emoticons 😢',
|
||||
'polarity': 'marl:Negative'
|
||||
}]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
easy_test()
|
40
example-plugins/basic_plugin.py
Normal file
40
example-plugins/basic_plugin.py
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/local/bin/python
|
||||
# coding: utf-8
|
||||
|
||||
from senpy import easy_test, SentimentBox, MappingMixin
|
||||
|
||||
import basic
|
||||
|
||||
|
||||
class Basic(MappingMixin, SentimentBox):
|
||||
'''Provides sentiment annotation using a lexicon'''
|
||||
|
||||
author = '@balkian'
|
||||
version = '0.1'
|
||||
|
||||
mappings = {
|
||||
'pos': 'marl:Positive',
|
||||
'neg': 'marl:Negative',
|
||||
'default': 'marl:Neutral'
|
||||
}
|
||||
|
||||
def predict_one(self, input):
|
||||
return basic.get_polarity(input)
|
||||
|
||||
test_cases = [{
|
||||
'input': 'Hello :)',
|
||||
'polarity': 'marl:Positive'
|
||||
}, {
|
||||
'input': 'So sad :(',
|
||||
'polarity': 'marl:Negative'
|
||||
}, {
|
||||
'input': 'Yay! Emojis 😁',
|
||||
'polarity': 'marl:Positive'
|
||||
}, {
|
||||
'input': 'But no emoticons 😢',
|
||||
'polarity': 'marl:Negative'
|
||||
}]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
easy_test()
|
106
example-plugins/configurable_plugin.py
Normal file
106
example-plugins/configurable_plugin.py
Normal file
@@ -0,0 +1,106 @@
|
||||
#!/usr/local/bin/python
|
||||
# coding: utf-8
|
||||
|
||||
from senpy import easy_test, models, plugins
|
||||
|
||||
import basic
|
||||
|
||||
|
||||
class Dictionary(plugins.SentimentPlugin):
|
||||
'''Sentiment annotation using a configurable lexicon'''
|
||||
|
||||
author = '@balkian'
|
||||
version = '0.2'
|
||||
|
||||
dictionaries = [basic.emojis, basic.emoticons]
|
||||
|
||||
mappings = {'pos': 'marl:Positive', 'neg': 'marl:Negative'}
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
polarity = basic.get_polarity(entry.text, self.dictionaries)
|
||||
if polarity in self.mappings:
|
||||
polarity = self.mappings[polarity]
|
||||
|
||||
s = models.Sentiment(marl__hasPolarity=polarity)
|
||||
s.prov(self)
|
||||
entry.sentiments.append(s)
|
||||
yield entry
|
||||
|
||||
test_cases = [{
|
||||
'input': 'Hello :)',
|
||||
'polarity': 'marl:Positive'
|
||||
}, {
|
||||
'input': 'So sad :(',
|
||||
'polarity': 'marl:Negative'
|
||||
}, {
|
||||
'input': 'Yay! Emojis 😁',
|
||||
'polarity': 'marl:Positive'
|
||||
}, {
|
||||
'input': 'But no emoticons 😢',
|
||||
'polarity': 'marl:Negative'
|
||||
}]
|
||||
|
||||
|
||||
class EmojiOnly(Dictionary):
|
||||
'''Sentiment annotation with a basic lexicon of emojis'''
|
||||
description = 'A plugin'
|
||||
dictionaries = [basic.emojis]
|
||||
|
||||
test_cases = [{
|
||||
'input': 'Hello :)',
|
||||
'polarity': 'marl:Neutral'
|
||||
}, {
|
||||
'input': 'So sad :(',
|
||||
'polarity': 'marl:Neutral'
|
||||
}, {
|
||||
'input': 'Yay! Emojis 😁',
|
||||
'polarity': 'marl:Positive'
|
||||
}, {
|
||||
'input': 'But no emoticons 😢',
|
||||
'polarity': 'marl:Negative'
|
||||
}]
|
||||
|
||||
|
||||
class EmoticonsOnly(Dictionary):
|
||||
'''Sentiment annotation with a basic lexicon of emoticons'''
|
||||
dictionaries = [basic.emoticons]
|
||||
|
||||
test_cases = [{
|
||||
'input': 'Hello :)',
|
||||
'polarity': 'marl:Positive'
|
||||
}, {
|
||||
'input': 'So sad :(',
|
||||
'polarity': 'marl:Negative'
|
||||
}, {
|
||||
'input': 'Yay! Emojis 😁',
|
||||
'polarity': 'marl:Neutral'
|
||||
}, {
|
||||
'input': 'But no emoticons 😢',
|
||||
'polarity': 'marl:Neutral'
|
||||
}]
|
||||
|
||||
|
||||
class Salutes(Dictionary):
|
||||
'''Sentiment annotation with a custom lexicon, for illustration purposes'''
|
||||
dictionaries = [{
|
||||
'marl:Positive': ['Hello', '!'],
|
||||
'marl:Negative': ['Good bye', ]
|
||||
}]
|
||||
|
||||
test_cases = [{
|
||||
'input': 'Hello :)',
|
||||
'polarity': 'marl:Positive'
|
||||
}, {
|
||||
'input': 'Good bye :(',
|
||||
'polarity': 'marl:Negative'
|
||||
}, {
|
||||
'input': 'Yay! Emojis 😁',
|
||||
'polarity': 'marl:Positive'
|
||||
}, {
|
||||
'input': 'But no emoticons 😢',
|
||||
'polarity': 'marl:Neutral'
|
||||
}]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
easy_test()
|
25
example-plugins/dummy_plugin.py
Normal file
25
example-plugins/dummy_plugin.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from senpy import AnalysisPlugin, easy
|
||||
|
||||
|
||||
class Dummy(AnalysisPlugin):
|
||||
'''This is a dummy self-contained plugin'''
|
||||
author = '@balkian'
|
||||
version = '0.1'
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
entry['nif:isString'] = entry['nif:isString'][::-1]
|
||||
entry.reversed = entry.get('reversed', 0) + 1
|
||||
yield entry
|
||||
|
||||
test_cases = [{
|
||||
'entry': {
|
||||
'nif:isString': 'Hello',
|
||||
},
|
||||
'expected': {
|
||||
'nif:isString': 'olleH'
|
||||
}
|
||||
}]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
easy()
|
40
example-plugins/dummy_required_plugin.py
Normal file
40
example-plugins/dummy_required_plugin.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from senpy import AnalysisPlugin, easy
|
||||
|
||||
|
||||
class DummyRequired(AnalysisPlugin):
|
||||
'''This is a dummy self-contained plugin'''
|
||||
author = '@balkian'
|
||||
version = '0.1'
|
||||
extra_params = {
|
||||
'example': {
|
||||
'description': 'An example parameter',
|
||||
'required': True,
|
||||
'options': ['a', 'b']
|
||||
}
|
||||
}
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
entry['nif:isString'] = entry['nif:isString'][::-1]
|
||||
entry.reversed = entry.get('reversed', 0) + 1
|
||||
yield entry
|
||||
|
||||
test_cases = [{
|
||||
'entry': {
|
||||
'nif:isString': 'Hello',
|
||||
},
|
||||
'should_fail': True
|
||||
}, {
|
||||
'entry': {
|
||||
'nif:isString': 'Hello',
|
||||
},
|
||||
'params': {
|
||||
'example': 'a'
|
||||
},
|
||||
'expected': {
|
||||
'nif:isString': 'olleH'
|
||||
}
|
||||
}]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
easy()
|
24
example-plugins/mynoop.py
Normal file
24
example-plugins/mynoop.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import noop
|
||||
from senpy.plugins import SentimentPlugin
|
||||
|
||||
|
||||
class NoOp(SentimentPlugin):
|
||||
'''This plugin does nothing. Literally nothing.'''
|
||||
|
||||
version = 0
|
||||
|
||||
def analyse_entry(self, entry, *args, **kwargs):
|
||||
yield entry
|
||||
|
||||
def test(self):
|
||||
print(dir(noop))
|
||||
super(NoOp, self).test()
|
||||
|
||||
test_cases = [{
|
||||
'entry': {
|
||||
'nif:isString': 'hello'
|
||||
},
|
||||
'expected': {
|
||||
'nif:isString': 'hello'
|
||||
}
|
||||
}]
|
3
example-plugins/mynoop.senpy
Normal file
3
example-plugins/mynoop.senpy
Normal file
@@ -0,0 +1,3 @@
|
||||
module: mynoop
|
||||
requirements:
|
||||
- noop
|
63
example-plugins/parameterized_plugin.py
Normal file
63
example-plugins/parameterized_plugin.py
Normal file
@@ -0,0 +1,63 @@
|
||||
#!/usr/local/bin/python
|
||||
# coding: utf-8
|
||||
|
||||
from senpy import easy_test, models, plugins
|
||||
|
||||
import basic
|
||||
|
||||
|
||||
class ParameterizedDictionary(plugins.SentimentPlugin):
|
||||
'''This is a basic self-contained plugin'''
|
||||
|
||||
author = '@balkian'
|
||||
version = '0.2'
|
||||
|
||||
extra_params = {
|
||||
'positive-words': {
|
||||
'description': 'Comma-separated list of words that are considered positive',
|
||||
'aliases': ['positive'],
|
||||
'required': True
|
||||
},
|
||||
'negative-words': {
|
||||
'description': 'Comma-separated list of words that are considered negative',
|
||||
'aliases': ['negative'],
|
||||
'required': False
|
||||
}
|
||||
}
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
positive_words = params['positive-words'].split(',')
|
||||
negative_words = params['negative-words'].split(',')
|
||||
dictionary = {
|
||||
'marl:Positive': positive_words,
|
||||
'marl:Negative': negative_words,
|
||||
}
|
||||
polarity = basic.get_polarity(entry.text, [dictionary])
|
||||
|
||||
s = models.Sentiment(marl__hasPolarity=polarity)
|
||||
s.prov(self)
|
||||
entry.sentiments.append(s)
|
||||
yield entry
|
||||
|
||||
test_cases = [
|
||||
{
|
||||
'input': 'Hello :)',
|
||||
'polarity': 'marl:Positive',
|
||||
'parameters': {
|
||||
'positive': "Hello,:)",
|
||||
'negative': "sad,:()"
|
||||
}
|
||||
},
|
||||
{
|
||||
'input': 'Hello :)',
|
||||
'polarity': 'marl:Negative',
|
||||
'parameters': {
|
||||
'positive': "",
|
||||
'negative': "Hello"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
easy_test()
|
33
example-plugins/sklearn/mydata.py
Normal file
33
example-plugins/sklearn/mydata.py
Normal file
@@ -0,0 +1,33 @@
|
||||
'''
|
||||
Create a dummy dataset.
|
||||
Messages with a happy emoticon are labelled positive
|
||||
Messages with a sad emoticon are labelled negative
|
||||
'''
|
||||
import random
|
||||
|
||||
dataset = []
|
||||
|
||||
vocabulary = ['hello', 'world', 'senpy', 'cool', 'goodbye', 'random', 'text']
|
||||
|
||||
emojimap = {
|
||||
1: [':)', ],
|
||||
-1: [':(', ]
|
||||
}
|
||||
|
||||
|
||||
for tag, values in emojimap.items():
|
||||
for i in range(1000):
|
||||
msg = ''
|
||||
for j in range(3):
|
||||
msg += random.choice(vocabulary)
|
||||
msg += " "
|
||||
msg += random.choice(values)
|
||||
dataset.append([msg, tag])
|
||||
|
||||
|
||||
text = []
|
||||
labels = []
|
||||
|
||||
for i in dataset:
|
||||
text.append(i[0])
|
||||
labels.append(i[1])
|
30
example-plugins/sklearn/mypipeline.py
Normal file
30
example-plugins/sklearn/mypipeline.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from sklearn.pipeline import Pipeline
|
||||
from sklearn.feature_extraction.text import CountVectorizer
|
||||
from sklearn.model_selection import train_test_split
|
||||
|
||||
from mydata import text, labels
|
||||
|
||||
X_train, X_test, y_train, y_test = train_test_split(text, labels, test_size=0.12, random_state=42)
|
||||
|
||||
from sklearn.naive_bayes import MultinomialNB
|
||||
|
||||
|
||||
count_vec = CountVectorizer(tokenizer=lambda x: x.split())
|
||||
clf3 = MultinomialNB()
|
||||
pipeline = Pipeline([('cv', count_vec),
|
||||
('clf', clf3)])
|
||||
|
||||
pipeline.fit(X_train, y_train)
|
||||
print('Feature names: {}'.format(count_vec.get_feature_names()))
|
||||
print('Class count: {}'.format(clf3.class_count_))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('--Results--')
|
||||
tests = [
|
||||
(['The sentiment for senpy should be positive :)', ], 1),
|
||||
(['The sentiment for anything else should be negative :()', ], -1)
|
||||
]
|
||||
for features, expected in tests:
|
||||
result = pipeline.predict(features)
|
||||
print('Input: {}\nExpected: {}\nGot: {}'.format(features[0], expected, result))
|
37
example-plugins/sklearn/pipeline_plugin.py
Normal file
37
example-plugins/sklearn/pipeline_plugin.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from senpy import SentimentBox, MappingMixin, easy_test
|
||||
|
||||
from mypipeline import pipeline
|
||||
|
||||
|
||||
class PipelineSentiment(MappingMixin, SentimentBox):
|
||||
'''
|
||||
This is a pipeline plugin that wraps a classifier defined in another module
|
||||
(mypipeline).
|
||||
'''
|
||||
author = '@balkian'
|
||||
version = 0.1
|
||||
maxPolarityValue = 1
|
||||
minPolarityValue = -1
|
||||
|
||||
mappings = {
|
||||
1: 'marl:Positive',
|
||||
-1: 'marl:Negative'
|
||||
}
|
||||
|
||||
def predict_one(self, input):
|
||||
return pipeline.predict([input, ])[0]
|
||||
|
||||
test_cases = [
|
||||
{
|
||||
'input': 'The sentiment for senpy should be positive :)',
|
||||
'polarity': 'marl:Positive'
|
||||
},
|
||||
{
|
||||
'input': 'The sentiment for senpy should be negative :(',
|
||||
'polarity': 'marl:Negative'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
easy_test()
|
27
example-plugins/sleep_plugin.py
Normal file
27
example-plugins/sleep_plugin.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from senpy.plugins import AnalysisPlugin
|
||||
from time import sleep
|
||||
|
||||
|
||||
class Sleep(AnalysisPlugin):
|
||||
'''Dummy plugin to test async'''
|
||||
author = "@balkian"
|
||||
version = "0.2"
|
||||
timeout = 0.05
|
||||
extra_params = {
|
||||
"timeout": {
|
||||
"@id": "timeout_sleep",
|
||||
"aliases": ["timeout", "to"],
|
||||
"required": False,
|
||||
"default": 0
|
||||
}
|
||||
}
|
||||
|
||||
def activate(self, *args, **kwargs):
|
||||
sleep(self.timeout)
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
sleep(float(params.get("timeout", self.timeout)))
|
||||
yield entry
|
||||
|
||||
def test(self):
|
||||
pass
|
1
extra-requirements.txt
Normal file
1
extra-requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
gsitk
|
@@ -13,7 +13,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: senpy-latest
|
||||
image: gsiupm/senpy:latest
|
||||
image: $IMAGEWTAG
|
||||
imagePullPolicy: Always
|
||||
args:
|
||||
- "--default-plugins"
|
||||
|
@@ -3,7 +3,6 @@ requests>=2.4.1
|
||||
tornado>=4.4.3
|
||||
PyLD>=0.6.5
|
||||
nltk
|
||||
six
|
||||
future
|
||||
jsonschema
|
||||
jsonref
|
||||
|
@@ -25,4 +25,10 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
logger.info('Using senpy version: {}'.format(__version__))
|
||||
|
||||
from .utils import easy, easy_load, easy_test # noqa: F401
|
||||
|
||||
from .models import * # noqa: F401,F403
|
||||
from .plugins import * # noqa: F401,F403
|
||||
from .extensions import * # noqa: F401,F403
|
||||
|
||||
__all__ = ['api', 'blueprints', 'cli', 'extensions', 'models', 'plugins']
|
||||
|
@@ -22,6 +22,7 @@ the server.
|
||||
|
||||
from flask import Flask
|
||||
from senpy.extensions import Senpy
|
||||
from senpy.utils import easy_test
|
||||
|
||||
import logging
|
||||
import os
|
||||
@@ -39,7 +40,7 @@ def main():
|
||||
'-l',
|
||||
metavar='logging_level',
|
||||
type=str,
|
||||
default="INFO",
|
||||
default="WARN",
|
||||
help='Logging level')
|
||||
parser.add_argument(
|
||||
'--debug',
|
||||
@@ -67,7 +68,7 @@ def main():
|
||||
'--plugins-folder',
|
||||
'-f',
|
||||
type=str,
|
||||
default='plugins',
|
||||
default='.',
|
||||
help='Where to look for plugins.')
|
||||
parser.add_argument(
|
||||
'--only-install',
|
||||
@@ -75,6 +76,18 @@ def main():
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Do not run a server, only install plugin dependencies')
|
||||
parser.add_argument(
|
||||
'--only-test',
|
||||
'-t',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Do not run a server, just test all plugins')
|
||||
parser.add_argument(
|
||||
'--only-list',
|
||||
'--list',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Do not run a server, only list plugins found')
|
||||
parser.add_argument(
|
||||
'--data-folder',
|
||||
'--data',
|
||||
@@ -97,7 +110,6 @@ def main():
|
||||
print('Senpy version {}'.format(senpy.__version__))
|
||||
print(sys.version)
|
||||
exit(1)
|
||||
logging.basicConfig()
|
||||
rl = logging.getLogger()
|
||||
rl.setLevel(getattr(logging, args.level))
|
||||
app = Flask(__name__)
|
||||
@@ -105,10 +117,21 @@ def main():
|
||||
sp = Senpy(app, args.plugins_folder,
|
||||
default_plugins=args.default_plugins,
|
||||
data_folder=args.data_folder)
|
||||
if args.only_list:
|
||||
plugins = sp.plugins()
|
||||
maxwidth = max(len(x.id) for x in plugins)
|
||||
for plugin in plugins:
|
||||
import inspect
|
||||
fpath = inspect.getfile(plugin.__class__)
|
||||
print('{: <{width}} @ {}'.format(plugin.id, fpath, width=maxwidth))
|
||||
return
|
||||
sp.install_deps()
|
||||
if args.only_install:
|
||||
return
|
||||
sp.activate_all()
|
||||
if args.only_test:
|
||||
easy_test(sp.plugins())
|
||||
return
|
||||
print('Senpy version {}'.format(senpy.__version__))
|
||||
print('Server running on port %s:%d. Ctrl+C to quit' % (args.host,
|
||||
args.port))
|
||||
|
64
senpy/api.py
64
senpy/api.py
@@ -13,8 +13,9 @@ API_PARAMS = {
|
||||
"expanded-jsonld": {
|
||||
"@id": "expanded-jsonld",
|
||||
"aliases": ["expanded"],
|
||||
"options": "boolean",
|
||||
"required": True,
|
||||
"default": 0
|
||||
"default": False
|
||||
},
|
||||
"with_parameters": {
|
||||
"aliases": ['withparameters',
|
||||
@@ -23,13 +24,6 @@ API_PARAMS = {
|
||||
"default": False,
|
||||
"required": True
|
||||
},
|
||||
"plugin_type": {
|
||||
"@id": "pluginType",
|
||||
"description": 'What kind of plugins to list',
|
||||
"aliases": ["pluginType"],
|
||||
"required": True,
|
||||
"default": "analysisPlugin"
|
||||
},
|
||||
"outformat": {
|
||||
"@id": "outformat",
|
||||
"aliases": ["o"],
|
||||
@@ -59,6 +53,31 @@ API_PARAMS = {
|
||||
}
|
||||
}
|
||||
|
||||
EVAL_PARAMS = {
|
||||
"algorithm": {
|
||||
"aliases": ["plug", "p", "plugins", "algorithms", 'algo', 'a', 'plugin'],
|
||||
"description": "Plugins to be evaluated",
|
||||
"required": True,
|
||||
"help": "See activated plugins in /plugins"
|
||||
},
|
||||
"dataset": {
|
||||
"aliases": ["datasets", "data", "d"],
|
||||
"description": "Datasets to be evaluated",
|
||||
"required": True,
|
||||
"help": "See avalaible datasets in /datasets"
|
||||
}
|
||||
}
|
||||
|
||||
PLUGINS_PARAMS = {
|
||||
"plugin_type": {
|
||||
"@id": "pluginType",
|
||||
"description": 'What kind of plugins to list',
|
||||
"aliases": ["pluginType"],
|
||||
"required": True,
|
||||
"default": 'analysisPlugin'
|
||||
}
|
||||
}
|
||||
|
||||
WEB_PARAMS = {
|
||||
"inHeaders": {
|
||||
"aliases": ["headers"],
|
||||
@@ -95,7 +114,7 @@ NIF_PARAMS = {
|
||||
"aliases": ["f"],
|
||||
"required": False,
|
||||
"default": "text",
|
||||
"options": ["turtle", "text", "json-ld"],
|
||||
"options": ["text", "json-ld"],
|
||||
},
|
||||
"language": {
|
||||
"@id": "language",
|
||||
@@ -126,39 +145,38 @@ def parse_params(indict, *specs):
|
||||
wrong_params = {}
|
||||
for spec in specs:
|
||||
for param, options in iteritems(spec):
|
||||
if param[0] != "@": # Exclude json-ld properties
|
||||
for alias in options.get("aliases", []):
|
||||
# Replace each alias with the correct name of the parameter
|
||||
if alias in indict and alias is not param:
|
||||
outdict[param] = indict[alias]
|
||||
del indict[alias]
|
||||
del outdict[alias]
|
||||
continue
|
||||
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:
|
||||
# We assume the default is correct
|
||||
outdict[param] = options["default"]
|
||||
elif "options" in spec[param]:
|
||||
if spec[param]["options"] == "boolean":
|
||||
elif options.get("required", False):
|
||||
wrong_params[param] = spec[param]
|
||||
continue
|
||||
if "options" in options:
|
||||
if options["options"] == "boolean":
|
||||
outdict[param] = outdict[param] in [None, True, 'true', '1']
|
||||
elif outdict[param] not in spec[param]["options"]:
|
||||
elif outdict[param] not in options["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",
|
||||
message='Missing or invalid parameters',
|
||||
parameters=outdict,
|
||||
errors={param: error
|
||||
for param, error in iteritems(wrong_params)})
|
||||
errors=wrong_params)
|
||||
raise message
|
||||
if 'algorithm' in outdict and not isinstance(outdict['algorithm'], list):
|
||||
outdict['algorithm'] = outdict['algorithm'].split(',')
|
||||
return outdict
|
||||
|
||||
|
||||
def get_extra_params(request, plugin=None):
|
||||
def parse_extra_params(request, plugin=None):
|
||||
params = request.parameters.copy()
|
||||
if plugin:
|
||||
extra_params = parse_params(params, plugin.get('extra_params', {}))
|
||||
@@ -176,7 +194,7 @@ def parse_call(params):
|
||||
results.entries.append(entry)
|
||||
elif params['informat'] == 'json-ld':
|
||||
results = from_string(params['input'], cls=Results)
|
||||
else:
|
||||
raise NotImplemented('Informat {} is not implemented'.format(params['informat']))
|
||||
else: # pragma: no cover
|
||||
raise NotImplementedError('Informat {} is not implemented'.format(params['informat']))
|
||||
results.parameters = params
|
||||
return results
|
||||
|
@@ -19,18 +19,19 @@ Blueprints for Senpy
|
||||
"""
|
||||
from flask import (Blueprint, request, current_app, render_template, url_for,
|
||||
jsonify)
|
||||
from .models import Error, Response, Help, Plugins, read_schema
|
||||
from .models import Error, Response, Help, Plugins, read_schema, Datasets
|
||||
from . import api
|
||||
from .version import __version__
|
||||
from functools import wraps
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
api_blueprint = Blueprint("api", __name__)
|
||||
demo_blueprint = Blueprint("demo", __name__)
|
||||
demo_blueprint = Blueprint("demo", __name__, template_folder='templates')
|
||||
ns_blueprint = Blueprint("ns", __name__)
|
||||
|
||||
|
||||
@@ -72,41 +73,52 @@ def schema(schema="definitions"):
|
||||
|
||||
|
||||
def basic_api(f):
|
||||
default_params = {
|
||||
'inHeaders': False,
|
||||
'expanded-jsonld': False,
|
||||
'outformat': 'json-ld',
|
||||
'with_parameters': True,
|
||||
}
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
raw_params = get_params(request)
|
||||
logger.info('Getting request: {}'.format(raw_params))
|
||||
headers = {'X-ORIGINAL-PARAMS': json.dumps(raw_params)}
|
||||
params = default_params
|
||||
|
||||
outformat = 'json-ld'
|
||||
try:
|
||||
print('Getting request:')
|
||||
print(request)
|
||||
params = api.parse_params(raw_params, api.WEB_PARAMS, api.API_PARAMS)
|
||||
if hasattr(request, 'parameters'):
|
||||
request.parameters.update(params)
|
||||
else:
|
||||
request.parameters = params
|
||||
response = f(*args, **kwargs)
|
||||
except Error as ex:
|
||||
response = ex
|
||||
response.parameters = params
|
||||
logger.error(ex)
|
||||
except (Exception) as ex:
|
||||
if current_app.debug:
|
||||
raise
|
||||
if not isinstance(ex, Error):
|
||||
msg = "{}:\n\t{}".format(ex,
|
||||
traceback.format_exc())
|
||||
ex = Error(message=msg, status=500)
|
||||
logger.exception('Error returning analysis result')
|
||||
response = ex
|
||||
response.parameters = raw_params
|
||||
logger.error(ex)
|
||||
|
||||
in_headers = params['inHeaders']
|
||||
expanded = params['expanded-jsonld']
|
||||
outformat = params['outformat']
|
||||
if 'parameters' in response and not params['with_parameters']:
|
||||
del response.parameters
|
||||
|
||||
logger.info('Response: {}'.format(response))
|
||||
return response.flask(
|
||||
in_headers=in_headers,
|
||||
in_headers=params['inHeaders'],
|
||||
headers=headers,
|
||||
prefix=url_for('.api_root', _external=True),
|
||||
context_uri=url_for('api.context',
|
||||
entity=type(response).__name__,
|
||||
_external=True),
|
||||
outformat=outformat,
|
||||
expanded=expanded)
|
||||
outformat=params['outformat'],
|
||||
expanded=params['expanded-jsonld'])
|
||||
|
||||
return decorated_function
|
||||
|
||||
@@ -116,21 +128,31 @@ def basic_api(f):
|
||||
def api_root():
|
||||
if request.parameters['help']:
|
||||
dic = dict(api.API_PARAMS, **api.NIF_PARAMS)
|
||||
response = Help(valid_parameters=dic)
|
||||
return response
|
||||
req = api.parse_call(request.parameters)
|
||||
return current_app.senpy.analyse(req)
|
||||
|
||||
@api_blueprint.route('/evaluate/', methods=['POST', 'GET'])
|
||||
@basic_api
|
||||
def evaluate():
|
||||
if request.parameters['help']:
|
||||
dic = dict(api.EVAL_PARAMS)
|
||||
response = Help(parameters=dic)
|
||||
return response
|
||||
else:
|
||||
req = api.parse_call(request.parameters)
|
||||
response = current_app.senpy.analyse(req)
|
||||
params = api.parse_params(request.parameters, api.EVAL_PARAMS)
|
||||
response = current_app.senpy.evaluate(params)
|
||||
return response
|
||||
|
||||
|
||||
@api_blueprint.route('/plugins/', methods=['POST', 'GET'])
|
||||
@basic_api
|
||||
def plugins():
|
||||
sp = current_app.senpy
|
||||
ptype = request.parameters.get('plugin_type')
|
||||
plugins = sp.filter_plugins(plugin_type=ptype)
|
||||
dic = Plugins(plugins=list(plugins.values()))
|
||||
params = api.parse_params(request.parameters, api.PLUGINS_PARAMS)
|
||||
ptype = params.get('plugin_type')
|
||||
plugins = list(sp.plugins(plugin_type=ptype))
|
||||
dic = Plugins(plugins=plugins)
|
||||
return dic
|
||||
|
||||
|
||||
@@ -138,12 +160,13 @@ def plugins():
|
||||
@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
|
||||
return sp.get_plugin(plugin)
|
||||
|
||||
|
||||
@api_blueprint.route('/datasets/', methods=['POST','GET'])
|
||||
@basic_api
|
||||
def datasets():
|
||||
sp = current_app.senpy
|
||||
datasets = sp.datasets
|
||||
dic = Datasets(datasets = list(datasets.values()))
|
||||
return dic
|
12
senpy/cli.py
12
senpy/cli.py
@@ -28,11 +28,15 @@ def main_function(argv):
|
||||
api.API_PARAMS,
|
||||
api.NIF_PARAMS)
|
||||
plugin_folder = params['plugin_folder']
|
||||
sp = Senpy(default_plugins=False, plugin_folder=plugin_folder)
|
||||
default_plugins = params.get('default-plugins', False)
|
||||
sp = Senpy(default_plugins=default_plugins, plugin_folder=plugin_folder)
|
||||
request = api.parse_call(params)
|
||||
algos = request.parameters.get('algorithm', sp.plugins.keys())
|
||||
algos = request.parameters.get('algorithm', None)
|
||||
if algos:
|
||||
for algo in algos:
|
||||
sp.activate_plugin(algo)
|
||||
else:
|
||||
sp.activate_all()
|
||||
res = sp.analyse(request)
|
||||
return res
|
||||
|
||||
@@ -42,9 +46,9 @@ def main():
|
||||
'''
|
||||
try:
|
||||
res = main_function(sys.argv[1:])
|
||||
print(res.to_JSON())
|
||||
print(res.serialize())
|
||||
except Error as err:
|
||||
print(err.to_JSON())
|
||||
print(err.serialize())
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import requests
|
||||
import logging
|
||||
from . import models
|
||||
from .plugins import default_plugin_type
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -13,10 +12,17 @@ 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
|
||||
def evaluate(self, input, method='GET', **kwargs):
|
||||
return self.request('/evaluate', method = method, input=input, **kwargs)
|
||||
|
||||
def plugins(self, *args, **kwargs):
|
||||
resp = self.request(path='/plugins').plugins
|
||||
return {p.name: p for p in resp}
|
||||
|
||||
def datasets(self):
|
||||
resp = self.request(path='/datasets').datasets
|
||||
return {d.name: d for d in resp}
|
||||
|
||||
def request(self, path=None, method='GET', **params):
|
||||
url = '{}{}'.format(self.endpoint, path)
|
||||
response = requests.request(method=method, url=url, params=params)
|
||||
|
@@ -6,55 +6,58 @@ from future import standard_library
|
||||
standard_library.install_aliases()
|
||||
|
||||
from . import plugins, api
|
||||
from .plugins import SenpyPlugin
|
||||
from .models import Error
|
||||
from .plugins import Plugin, evaluate
|
||||
from .models import Error, AggregatedEvaluation
|
||||
from .blueprints import api_blueprint, demo_blueprint, ns_blueprint
|
||||
|
||||
from threading import Thread
|
||||
from functools import partial
|
||||
|
||||
import os
|
||||
import copy
|
||||
import errno
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from gsitk.datasets.datasets import DatasetManager
|
||||
GSITK_AVAILABLE = True
|
||||
except ImportError:
|
||||
logger.warn('GSITK is not installed. Some functions will be unavailable.')
|
||||
GSITK_AVAILABLE = False
|
||||
|
||||
|
||||
class Senpy(object):
|
||||
""" Default Senpy extension for Flask """
|
||||
|
||||
def __init__(self,
|
||||
app=None,
|
||||
plugin_folder=".",
|
||||
data_folder=None,
|
||||
default_plugins=False):
|
||||
self.app = app
|
||||
self._search_folders = set()
|
||||
self._plugin_list = []
|
||||
self._outdated = True
|
||||
self._default = None
|
||||
|
||||
default_data = os.path.join(os.getcwd(), 'senpy_data')
|
||||
self.data_folder = data_folder or os.environ.get('SENPY_DATA', default_data)
|
||||
try:
|
||||
os.makedirs(self.data_folder)
|
||||
except OSError as e:
|
||||
if e.errno == errno.EEXIST:
|
||||
logger.debug('Data folder exists: {}'.format(self.data_folder))
|
||||
else: # pragma: no cover
|
||||
raise
|
||||
|
||||
self._default = None
|
||||
self._plugins = {}
|
||||
if plugin_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)
|
||||
|
||||
self.data_folder = data_folder or os.environ.get('SENPY_DATA',
|
||||
os.path.join(os.getcwd(),
|
||||
'senpy_data'))
|
||||
try:
|
||||
os.makedirs(self.data_folder)
|
||||
except OSError as e:
|
||||
if e.errno == errno.EEXIST:
|
||||
print('Directory not created.')
|
||||
else:
|
||||
raise
|
||||
|
||||
self.app = app
|
||||
if app is not None:
|
||||
self.init_app(app)
|
||||
|
||||
@@ -69,21 +72,52 @@ class Senpy(object):
|
||||
# otherwise fall back to the request context
|
||||
if hasattr(app, 'teardown_appcontext'):
|
||||
app.teardown_appcontext(self.teardown)
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
app.teardown_request(self.teardown)
|
||||
app.register_blueprint(api_blueprint, url_prefix="/api")
|
||||
app.register_blueprint(ns_blueprint, url_prefix="/ns")
|
||||
app.register_blueprint(demo_blueprint, url_prefix="/")
|
||||
|
||||
def add_plugin(self, plugin):
|
||||
self._plugins[plugin.name.lower()] = plugin
|
||||
|
||||
def delete_plugin(self, plugin):
|
||||
del self._plugins[plugin.name.lower()]
|
||||
|
||||
def plugins(self, **kwargs):
|
||||
""" Return the plugins registered for a given application. Filtered by criteria """
|
||||
return list(plugins.pfilter(self._plugins, **kwargs))
|
||||
|
||||
def get_plugin(self, name, default=None):
|
||||
if name == 'default':
|
||||
return self.default_plugin
|
||||
plugin = name.lower()
|
||||
if plugin in self._plugins:
|
||||
return self._plugins[plugin]
|
||||
|
||||
results = self.plugins(id='plugins/{}'.format(name))
|
||||
|
||||
if not results:
|
||||
return Error(message="Plugin not found", status=404)
|
||||
return results[0]
|
||||
|
||||
@property
|
||||
def analysis_plugins(self):
|
||||
""" Return only the analysis plugins """
|
||||
return self.plugins(plugin_type='analysisPlugin')
|
||||
|
||||
def add_folder(self, folder, from_root=False):
|
||||
""" Find plugins in this folder and add them to this instance """
|
||||
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
|
||||
new_plugins = plugins.from_folder([folder],
|
||||
data_folder=self.data_folder)
|
||||
for plugin in new_plugins:
|
||||
self.add_plugin(plugin)
|
||||
else:
|
||||
logger.debug("Not a folder: %s", folder)
|
||||
raise AttributeError("Not a folder or does not exist: %s", folder)
|
||||
|
||||
def _get_plugins(self, request):
|
||||
if not self.analysis_plugins:
|
||||
@@ -102,14 +136,16 @@ class Senpy(object):
|
||||
|
||||
plugins = list()
|
||||
for algo in algos:
|
||||
if algo not in self.plugins:
|
||||
logger.debug(("The algorithm '{}' is not valid\n"
|
||||
algo = algo.lower()
|
||||
if algo not in self._plugins:
|
||||
msg = ("The algorithm '{}' is not valid\n"
|
||||
"Valid algorithms: {}").format(algo,
|
||||
self.plugins.keys()))
|
||||
self._plugins.keys())
|
||||
logger.debug(msg)
|
||||
raise Error(
|
||||
status=404,
|
||||
message="The algorithm '{}' is not valid".format(algo))
|
||||
plugins.append(self.plugins[algo])
|
||||
message=msg)
|
||||
plugins.append(self._plugins[algo])
|
||||
return plugins
|
||||
|
||||
def _process_entries(self, entries, req, plugins):
|
||||
@@ -123,7 +159,7 @@ class Senpy(object):
|
||||
return
|
||||
plugin = plugins[0]
|
||||
self._activate(plugin) # Make sure the plugin is activated
|
||||
specific_params = api.get_extra_params(req, plugin)
|
||||
specific_params = api.parse_extra_params(req, plugin)
|
||||
req.analysis.append({'plugin': plugin,
|
||||
'parameters': specific_params})
|
||||
results = plugin.analyse_entries(entries, specific_params)
|
||||
@@ -131,7 +167,7 @@ class Senpy(object):
|
||||
yield i
|
||||
|
||||
def install_deps(self):
|
||||
for plugin in self.filter_plugins(is_activated=True):
|
||||
for plugin in self.plugins(is_activated=True):
|
||||
plugins.install_deps(plugin)
|
||||
|
||||
def analyse(self, request):
|
||||
@@ -141,7 +177,6 @@ class Senpy(object):
|
||||
by api.parse_call().
|
||||
"""
|
||||
logger.debug("analysing request: {}".format(request))
|
||||
try:
|
||||
entries = request.entries
|
||||
request.entries = []
|
||||
plugins = self._get_plugins(request)
|
||||
@@ -149,28 +184,67 @@ class Senpy(object):
|
||||
for i in self._process_entries(entries, results, plugins):
|
||||
results.entries.append(i)
|
||||
self.convert_emotions(results)
|
||||
if 'with_parameters' not in results.parameters:
|
||||
del results.parameters
|
||||
logger.debug("Returning analysis result: {}".format(results))
|
||||
except (Error, Exception) as ex:
|
||||
if not isinstance(ex, Error):
|
||||
msg = "Error during analysis: {} \n\t{}".format(ex,
|
||||
traceback.format_exc())
|
||||
ex = Error(message=msg, status=500)
|
||||
logger.exception('Error returning analysis result')
|
||||
raise ex
|
||||
results.analysis = [i['plugin'].id for i in results.analysis]
|
||||
return results
|
||||
|
||||
def _get_datasets(self, request):
|
||||
if not self.datasets:
|
||||
raise Error(
|
||||
status=404,
|
||||
message=("No datasets found."
|
||||
" Please verify DatasetManager"))
|
||||
datasets_name = request.parameters.get('dataset', None).split(',')
|
||||
for dataset in datasets_name:
|
||||
if dataset not in self.datasets:
|
||||
logger.debug(("The dataset '{}' is not valid\n"
|
||||
"Valid datasets: {}").format(dataset,
|
||||
self.datasets.keys()))
|
||||
raise Error(
|
||||
status=404,
|
||||
message="The dataset '{}' is not valid".format(dataset))
|
||||
dm = DatasetManager()
|
||||
datasets = dm.prepare_datasets(datasets_name)
|
||||
return datasets
|
||||
|
||||
@property
|
||||
def datasets(self):
|
||||
if not GSITK_AVAILABLE:
|
||||
raise Exception('GSITK is not available. Install it to use this function.')
|
||||
self._dataset_list = {}
|
||||
dm = DatasetManager()
|
||||
for item in dm.get_datasets():
|
||||
for key in item:
|
||||
if key in self._dataset_list:
|
||||
continue
|
||||
properties = item[key]
|
||||
properties['@id'] = key
|
||||
self._dataset_list[key] = properties
|
||||
return self._dataset_list
|
||||
|
||||
def evaluate(self, params):
|
||||
if not GSITK_AVAILABLE:
|
||||
raise Exception('GSITK is not available. Install it to use this function.')
|
||||
logger.debug("evaluating request: {}".format(params))
|
||||
results = AggregatedEvaluation()
|
||||
results.parameters = params
|
||||
datasets = self._get_datasets(results)
|
||||
plugins = self._get_plugins(results)
|
||||
for eval in evaluate(plugins, datasets):
|
||||
results.evaluations.append(eval)
|
||||
if 'with_parameters' not in results.parameters:
|
||||
del results.parameters
|
||||
logger.debug("Returning evaluation result: {}".format(results))
|
||||
return results
|
||||
|
||||
def _conversion_candidates(self, fromModel, toModel):
|
||||
candidates = self.filter_plugins(plugin_type='emotionConversionPlugin')
|
||||
for name, candidate in candidates.items():
|
||||
candidates = self.plugins(plugin_type='emotionConversionPlugin')
|
||||
for candidate in candidates:
|
||||
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):
|
||||
@@ -197,7 +271,8 @@ class Senpy(object):
|
||||
logger.debug('Analysis plugin {} uses model: {}'.format(plugin.id, fromModel))
|
||||
except StopIteration:
|
||||
e = Error(('No conversion plugin found for: '
|
||||
'{} -> {}'.format(fromModel, toModel)))
|
||||
'{} -> {}'.format(fromModel, toModel)),
|
||||
status=404)
|
||||
e.original_response = resp
|
||||
e.parameters = params
|
||||
raise e
|
||||
@@ -223,36 +298,40 @@ class Senpy(object):
|
||||
|
||||
@property
|
||||
def default_plugin(self):
|
||||
candidate = self._default
|
||||
if not candidate:
|
||||
candidates = self.filter_plugins(plugin_type='analysisPlugin',
|
||||
if not self._default or not self._default.is_activated:
|
||||
candidates = self.plugins(plugin_type='analysisPlugin',
|
||||
is_activated=True)
|
||||
if len(candidates) > 0:
|
||||
candidate = list(candidates.values())[0]
|
||||
logger.debug("Default: {}".format(candidate))
|
||||
return candidate
|
||||
self._default = candidates[0]
|
||||
else:
|
||||
self._default = None
|
||||
logger.debug("Default: {}".format(self._default))
|
||||
return self._default
|
||||
|
||||
@default_plugin.setter
|
||||
def default_plugin(self, value):
|
||||
if isinstance(value, SenpyPlugin):
|
||||
if isinstance(value, Plugin):
|
||||
if not value.is_activated:
|
||||
raise AttributeError('The default plugin has to be activated.')
|
||||
self._default = value
|
||||
|
||||
else:
|
||||
self._default = self.plugins[value]
|
||||
self._default = self._plugins[value.lower()]
|
||||
|
||||
def activate_all(self, sync=True):
|
||||
ps = []
|
||||
for plug in self.plugins.keys():
|
||||
for plug in self._plugins.keys():
|
||||
ps.append(self.activate_plugin(plug, sync=sync))
|
||||
return ps
|
||||
|
||||
def deactivate_all(self, sync=True):
|
||||
ps = []
|
||||
for plug in self.plugins.keys():
|
||||
for plug in self._plugins.keys():
|
||||
ps.append(self.deactivate_plugin(plug, sync=sync))
|
||||
return ps
|
||||
|
||||
def _set_active(self, plugin, active=True, *args, **kwargs):
|
||||
''' We're using a variable in the plugin itself to activate/deactive plugins.\
|
||||
''' We're using a variable in the plugin itself to activate/deactivate plugins.\
|
||||
Note that plugins may activate themselves by setting this variable.
|
||||
'''
|
||||
plugin.is_activated = active
|
||||
@@ -262,24 +341,18 @@ class Senpy(object):
|
||||
with plugin._lock:
|
||||
if plugin.is_activated:
|
||||
return
|
||||
try:
|
||||
plugin.activate()
|
||||
msg = "Plugin activated: {}".format(plugin.name)
|
||||
logger.info(msg)
|
||||
success = True
|
||||
self._set_active(plugin, success)
|
||||
except Exception as ex:
|
||||
msg = "Error activating plugin {} - {} : \n\t{}".format(
|
||||
plugin.name, ex, traceback.format_exc())
|
||||
logger.error(msg)
|
||||
raise Error(msg)
|
||||
|
||||
def activate_plugin(self, plugin_name, sync=True):
|
||||
try:
|
||||
plugin = self.plugins[plugin_name]
|
||||
except KeyError:
|
||||
plugin_name = plugin_name.lower()
|
||||
if plugin_name not in self._plugins:
|
||||
raise Error(
|
||||
message="Plugin not found: {}".format(plugin_name), status=404)
|
||||
plugin = self._plugins[plugin_name]
|
||||
|
||||
logger.info("Activating plugin: {}".format(plugin.name))
|
||||
|
||||
@@ -294,20 +367,15 @@ class Senpy(object):
|
||||
with plugin._lock:
|
||||
if not plugin.is_activated:
|
||||
return
|
||||
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()))
|
||||
|
||||
def deactivate_plugin(self, plugin_name, sync=True):
|
||||
try:
|
||||
plugin = self.plugins[plugin_name]
|
||||
except KeyError:
|
||||
plugin_name = plugin_name.lower()
|
||||
if plugin_name not in self._plugins:
|
||||
raise Error(
|
||||
message="Plugin not found: {}".format(plugin_name), status=404)
|
||||
plugin = self._plugins[plugin_name]
|
||||
|
||||
self._set_active(plugin, False)
|
||||
|
||||
@@ -320,20 +388,3 @@ class Senpy(object):
|
||||
|
||||
def teardown(self, exception):
|
||||
pass
|
||||
|
||||
@property
|
||||
def plugins(self):
|
||||
""" Return the plugins registered for a given application. """
|
||||
if self._outdated:
|
||||
self._plugin_list = plugins.load_plugins(self._search_folders,
|
||||
data_folder=self.data_folder)
|
||||
self._outdated = False
|
||||
return self._plugin_list
|
||||
|
||||
def filter_plugins(self, **kwargs):
|
||||
return plugins.pfilter(self.plugins, **kwargs)
|
||||
|
||||
@property
|
||||
def analysis_plugins(self):
|
||||
""" Return only the analysis plugins """
|
||||
return self.filter_plugins(plugin_type='analysisPlugin')
|
||||
|
257
senpy/meta.py
Normal file
257
senpy/meta.py
Normal file
@@ -0,0 +1,257 @@
|
||||
'''
|
||||
Meta-programming for the models.
|
||||
'''
|
||||
import os
|
||||
import json
|
||||
import jsonschema
|
||||
import inspect
|
||||
import copy
|
||||
|
||||
from abc import ABCMeta
|
||||
from collections import MutableMapping, namedtuple
|
||||
|
||||
|
||||
class BaseMeta(ABCMeta):
|
||||
'''
|
||||
Metaclass for models. It extracts the default values for the fields in
|
||||
the model.
|
||||
|
||||
For instance, instances of the following class wouldn't need to mark
|
||||
their version or description on initialization:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyPlugin(Plugin):
|
||||
version=0.3
|
||||
description='A dull plugin'
|
||||
|
||||
|
||||
Note that these operations could be included in the __init__ of the
|
||||
class, but it would be very inefficient.
|
||||
'''
|
||||
_subtypes = {}
|
||||
|
||||
def __new__(mcs, name, bases, attrs, **kwargs):
|
||||
register_afterwards = False
|
||||
defaults = {}
|
||||
|
||||
attrs = mcs.expand_with_schema(name, attrs)
|
||||
if 'schema' in attrs:
|
||||
register_afterwards = True
|
||||
for base in bases:
|
||||
if hasattr(base, '_defaults'):
|
||||
defaults.update(getattr(base, '_defaults'))
|
||||
|
||||
info, rest = mcs.split_attrs(attrs)
|
||||
|
||||
for i in list(info.keys()):
|
||||
if isinstance(info[i], _Alias):
|
||||
fget, fset, fdel = make_property(info[i].indict)
|
||||
rest[i] = property(fget=fget, fset=fset, fdel=fdel)
|
||||
else:
|
||||
defaults[i] = info[i]
|
||||
|
||||
rest['_defaults'] = defaults
|
||||
|
||||
cls = super(BaseMeta, mcs).__new__(mcs, name, tuple(bases), rest)
|
||||
|
||||
if register_afterwards:
|
||||
mcs.register(cls, defaults['@type'])
|
||||
return cls
|
||||
|
||||
@classmethod
|
||||
def register(mcs, rsubclass, rtype=None):
|
||||
mcs._subtypes[rtype or rsubclass.__name__] = rsubclass
|
||||
|
||||
@staticmethod
|
||||
def expand_with_schema(name, attrs):
|
||||
if 'schema' in attrs: # Schema specified by name
|
||||
schema_file = '{}.json'.format(attrs['schema'])
|
||||
elif 'schema_file' in attrs:
|
||||
schema_file = attrs['schema_file']
|
||||
del attrs['schema_file']
|
||||
else:
|
||||
return attrs
|
||||
|
||||
if '/' not in 'schema_file':
|
||||
thisdir = os.path.dirname(os.path.realpath(__file__))
|
||||
schema_file = os.path.join(thisdir,
|
||||
'schemas',
|
||||
schema_file)
|
||||
|
||||
schema_path = 'file://' + schema_file
|
||||
|
||||
with open(schema_file) as f:
|
||||
schema = json.load(f)
|
||||
|
||||
resolver = jsonschema.RefResolver(schema_path, schema)
|
||||
attrs['@type'] = "".join((name[0].lower(), name[1:]))
|
||||
attrs['_schema_file'] = schema_file
|
||||
attrs['schema'] = schema
|
||||
attrs['_validator'] = jsonschema.Draft4Validator(schema, resolver=resolver)
|
||||
|
||||
schema_defaults = BaseMeta.get_defaults(attrs['schema'])
|
||||
attrs.update(schema_defaults)
|
||||
|
||||
return attrs
|
||||
|
||||
@staticmethod
|
||||
def is_func(v):
|
||||
return inspect.isroutine(v) or inspect.ismethod(v) or \
|
||||
inspect.ismodule(v) or isinstance(v, property)
|
||||
|
||||
@staticmethod
|
||||
def is_internal(k):
|
||||
return k[0] == '_' or k == 'schema' or k == 'data'
|
||||
|
||||
@staticmethod
|
||||
def get_key(key):
|
||||
if key[0] != '_':
|
||||
key = key.replace("__", ":", 1)
|
||||
return key
|
||||
|
||||
@staticmethod
|
||||
def split_attrs(attrs):
|
||||
'''
|
||||
Extract the attributes of the class.
|
||||
|
||||
This allows adding default values in the class definition.
|
||||
e.g.:
|
||||
'''
|
||||
isattr = {}
|
||||
rest = {}
|
||||
for key, value in attrs.items():
|
||||
if not (BaseMeta.is_internal(key)) and (not BaseMeta.is_func(value)):
|
||||
isattr[key] = value
|
||||
else:
|
||||
rest[key] = value
|
||||
return isattr, rest
|
||||
|
||||
@staticmethod
|
||||
def get_defaults(schema):
|
||||
temp = {}
|
||||
for obj in [
|
||||
schema,
|
||||
] + schema.get('allOf', []):
|
||||
for k, v in obj.get('properties', {}).items():
|
||||
if 'default' in v and k not in temp:
|
||||
temp[k] = v['default']
|
||||
return temp
|
||||
|
||||
|
||||
def make_property(key):
|
||||
|
||||
def fget(self):
|
||||
return self[key]
|
||||
|
||||
def fdel(self):
|
||||
del self[key]
|
||||
|
||||
def fset(self, value):
|
||||
self[key] = value
|
||||
|
||||
return fget, fset, fdel
|
||||
|
||||
|
||||
class CustomDict(MutableMapping, object):
|
||||
'''
|
||||
A dictionary whose elements can also be accessed as attributes. Since some
|
||||
characters are not valid in the dot-notation, the attribute names also
|
||||
converted. e.g.:
|
||||
|
||||
> d = CustomDict()
|
||||
> d.key = d['ns:name'] = 1
|
||||
> d.key == d['key']
|
||||
True
|
||||
> d.ns__name == d['ns:name']
|
||||
'''
|
||||
|
||||
_defaults = {}
|
||||
_map_attr_key = {'id': '@id'}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CustomDict, self).__init__()
|
||||
for k, v in self._defaults.items():
|
||||
self[k] = copy.copy(v)
|
||||
for arg in args:
|
||||
self.update(arg)
|
||||
for k, v in kwargs.items():
|
||||
self[self._attr_to_key(k)] = v
|
||||
return self
|
||||
|
||||
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) or isinstance(item, set):
|
||||
return list(ser_or_down(i) for i in item)
|
||||
else:
|
||||
return item
|
||||
|
||||
return ser_or_down(self.as_dict())
|
||||
|
||||
def __getitem__(self, key):
|
||||
key = self._key_to_attr(key)
|
||||
return self.__dict__[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
'''Do not insert data directly, there might be a property in that key. '''
|
||||
key = self._key_to_attr(key)
|
||||
return setattr(self, key, value)
|
||||
|
||||
def as_dict(self):
|
||||
return {self._attr_to_key(k): v for k, v in self.__dict__.items()
|
||||
if not self._internal_key(k)}
|
||||
|
||||
def __iter__(self):
|
||||
return (k for k in self.__dict__ if not self._internal_key(k))
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__dict__)
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self.__dict__[key]
|
||||
|
||||
def update(self, other):
|
||||
for k, v in other.items():
|
||||
self[k] = v
|
||||
|
||||
def _attr_to_key(self, key):
|
||||
key = key.replace("__", ":", 1)
|
||||
key = self._map_attr_key.get(key, key)
|
||||
return key
|
||||
|
||||
def _key_to_attr(self, key):
|
||||
if self._internal_key(key):
|
||||
return key
|
||||
key = key.replace(":", "__", 1)
|
||||
return key
|
||||
|
||||
def __getattr__(self, key):
|
||||
try:
|
||||
return self.__dict__[self._attr_to_key(key)]
|
||||
except KeyError:
|
||||
raise AttributeError
|
||||
|
||||
@staticmethod
|
||||
def _internal_key(key):
|
||||
return key[0] == '_'
|
||||
|
||||
def __str__(self):
|
||||
return str(self.serializable())
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.serializable())
|
||||
|
||||
|
||||
_Alias = namedtuple('Alias', 'indict')
|
||||
|
||||
|
||||
def alias(key):
|
||||
return _Alias(key)
|
329
senpy/models.py
329
senpy/models.py
@@ -6,24 +6,30 @@ 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
|
||||
from future import standard_library
|
||||
standard_library.install_aliases()
|
||||
|
||||
from future.utils import with_metaclass
|
||||
from past.builtins import basestring
|
||||
|
||||
import time
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import jsonref
|
||||
import jsonschema
|
||||
|
||||
from flask import Response as FlaskResponse
|
||||
from pyld import jsonld
|
||||
|
||||
from rdflib import Graph
|
||||
|
||||
import logging
|
||||
|
||||
logging.getLogger('rdflib').setLevel(logging.WARN)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from rdflib import Graph
|
||||
|
||||
|
||||
from .meta import BaseMeta, CustomDict, alias
|
||||
|
||||
DEFINITIONS_FILE = 'definitions.json'
|
||||
CONTEXT_PATH = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)), 'schemas', 'context.jsonld')
|
||||
@@ -45,38 +51,96 @@ def read_schema(schema_file, absolute=False):
|
||||
return jsonref.load(f, base_uri=schema_uri)
|
||||
|
||||
|
||||
base_schema = read_schema(DEFINITIONS_FILE)
|
||||
|
||||
|
||||
class Context(dict):
|
||||
@staticmethod
|
||||
def load(context):
|
||||
def load_context(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))
|
||||
contexts.append(load_context(c))
|
||||
return contexts
|
||||
elif isinstance(context, dict):
|
||||
return Context(context)
|
||||
elif isinstance(context, string_types):
|
||||
return dict(context)
|
||||
elif isinstance(context, basestring):
|
||||
try:
|
||||
with open(context) as f:
|
||||
return Context(json.loads(f.read()))
|
||||
return dict(json.loads(f.read()))
|
||||
except IOError:
|
||||
return context
|
||||
else:
|
||||
raise AttributeError('Please, provide a valid context')
|
||||
|
||||
|
||||
base_context = Context.load(CONTEXT_PATH)
|
||||
base_context = load_context(CONTEXT_PATH)
|
||||
|
||||
|
||||
class SenpyMixin(object):
|
||||
def register(rsubclass, rtype=None):
|
||||
BaseMeta.register(rsubclass, rtype)
|
||||
|
||||
|
||||
class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
||||
'''
|
||||
Entities of the base model are a special kind of dictionary that emulates
|
||||
a JSON-LD object. The structure of the dictionary is checked via JSON-schema.
|
||||
For convenience, the values can also be accessed as attributes
|
||||
(a la Javascript). e.g.:
|
||||
|
||||
>>> myobject.key == myobject['key']
|
||||
True
|
||||
>>> myobject.ns__name == myobject['ns:name']
|
||||
True
|
||||
|
||||
Additionally, subclasses of this class can specify default values for their
|
||||
instances. These defaults are inherited by subclasses. e.g.:
|
||||
|
||||
>>> class NewModel(BaseModel):
|
||||
... mydefault = 5
|
||||
>>> n1 = NewModel()
|
||||
>>> n1['mydefault'] == 5
|
||||
True
|
||||
>>> n1.mydefault = 3
|
||||
>>> n1['mydefault'] = 3
|
||||
True
|
||||
>>> n2 = NewModel()
|
||||
>>> n2 == 5
|
||||
True
|
||||
>>> class SubModel(NewModel):
|
||||
pass
|
||||
>>> subn = SubModel()
|
||||
>>> subn.mydefault == 5
|
||||
True
|
||||
|
||||
Lastly, every subclass that also specifies a schema will get registered, so it
|
||||
is possible to deserialize JSON and get the right type.
|
||||
i.e. to recover an instance of the original class from a plain JSON.
|
||||
|
||||
'''
|
||||
|
||||
schema_file = DEFINITIONS_FILE
|
||||
_context = base_context["@context"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
auto_id = kwargs.pop('_auto_id', True)
|
||||
|
||||
super(BaseModel, self).__init__(*args, **kwargs)
|
||||
|
||||
if auto_id:
|
||||
self.id
|
||||
|
||||
if '@type' not in self:
|
||||
logger.warn('Created an instance of an unknown model')
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
if '@id' not in self:
|
||||
self['@id'] = ':{}_{}'.format(type(self).__name__, time.time())
|
||||
return self['@id']
|
||||
|
||||
@id.setter
|
||||
def id(self, value):
|
||||
self['@id'] = value
|
||||
|
||||
def flask(self,
|
||||
in_headers=True,
|
||||
headers=None,
|
||||
@@ -102,7 +166,7 @@ class SenpyMixin(object):
|
||||
})
|
||||
return FlaskResponse(
|
||||
response=content,
|
||||
status=getattr(self, "status", 200),
|
||||
status=self.get('status', 200),
|
||||
headers=headers,
|
||||
mimetype=mimetype)
|
||||
|
||||
@@ -130,25 +194,8 @@ class SenpyMixin(object):
|
||||
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) or isinstance(item, set):
|
||||
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,
|
||||
with_context=False,
|
||||
context_uri=None,
|
||||
prefix=None,
|
||||
expanded=False):
|
||||
@@ -172,10 +219,6 @@ class SenpyMixin(object):
|
||||
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
|
||||
@@ -183,86 +226,22 @@ class SenpyMixin(object):
|
||||
obj = obj.jsonld()
|
||||
self._validator.validate(obj)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.serialize())
|
||||
def prov(self, another):
|
||||
self['prov:wasGeneratedBy'] = another.id
|
||||
|
||||
|
||||
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):
|
||||
if key is 'id':
|
||||
key = '@id'
|
||||
key = key.replace("__", ":", 1)
|
||||
return key
|
||||
|
||||
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):
|
||||
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] != "_"}
|
||||
return d
|
||||
|
||||
|
||||
def register(rsubclass, rtype=None):
|
||||
_subtypes[rtype or rsubclass.__name__] = rsubclass
|
||||
|
||||
|
||||
_subtypes = {}
|
||||
def subtypes():
|
||||
return BaseMeta._subtypes
|
||||
|
||||
|
||||
def from_dict(indict, cls=None):
|
||||
if not cls:
|
||||
target = indict.get('@type', None)
|
||||
cls = BaseModel
|
||||
try:
|
||||
if target and target in _subtypes:
|
||||
cls = _subtypes[target]
|
||||
else:
|
||||
cls = BaseModel
|
||||
except Exception:
|
||||
cls = BaseModel
|
||||
cls = subtypes()[target]
|
||||
except KeyError:
|
||||
pass
|
||||
outdict = dict()
|
||||
for k, v in indict.items():
|
||||
if k == '@context':
|
||||
@@ -270,10 +249,11 @@ def from_dict(indict, cls=None):
|
||||
elif isinstance(v, dict):
|
||||
v = from_dict(indict[k])
|
||||
elif isinstance(v, list):
|
||||
v = v[:]
|
||||
for ix, v2 in enumerate(v):
|
||||
if isinstance(v2, dict):
|
||||
v[ix] = from_dict(v2)
|
||||
outdict[k] = v
|
||||
outdict[k] = copy.copy(v)
|
||||
return cls(**outdict)
|
||||
|
||||
|
||||
@@ -286,37 +266,54 @@ def from_json(injson):
|
||||
return from_dict(indict)
|
||||
|
||||
|
||||
def from_schema(name, schema=None, schema_file=None, base_classes=None):
|
||||
class Entry(BaseModel):
|
||||
schema = 'entry'
|
||||
|
||||
text = alias('nif:isString')
|
||||
|
||||
|
||||
class Sentiment(BaseModel):
|
||||
schema = 'sentiment'
|
||||
|
||||
polarity = alias('marl:hasPolarity')
|
||||
polarityValue = alias('marl:hasPolarityValue')
|
||||
|
||||
|
||||
class Error(BaseModel, Exception):
|
||||
schema = 'error'
|
||||
|
||||
def __init__(self, message='Generic senpy exception', *args, **kwargs):
|
||||
Exception.__init__(self, message)
|
||||
super(Error, self).__init__(*args, **kwargs)
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
if not hasattr(self, 'errors'):
|
||||
return self.message
|
||||
return '{}:\n\t{}'.format(self.message, self.errors)
|
||||
|
||||
def __hash__(self):
|
||||
return Exception.__hash__(self)
|
||||
|
||||
|
||||
# Add the remaining schemas programmatically
|
||||
|
||||
def _class_from_schema(name, schema=None, 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:])
|
||||
if '/' not in 'schema_file':
|
||||
schema_file = os.path.join(os.path.dirname(os.path.realpath(__file__)),
|
||||
'schemas',
|
||||
schema_file)
|
||||
|
||||
schema_path = 'file://' + schema_file
|
||||
|
||||
with open(schema_file) as f:
|
||||
schema = json.load(f)
|
||||
|
||||
dct = {}
|
||||
|
||||
resolver = jsonschema.RefResolver(schema_path, schema)
|
||||
dct['@type'] = name
|
||||
dct['_schema_file'] = schema_file
|
||||
dct['schema'] = schema
|
||||
dct['_validator'] = jsonschema.Draft4Validator(schema, resolver=resolver)
|
||||
|
||||
newclass = type(class_name, tuple(base_classes), dct)
|
||||
|
||||
register(newclass, name)
|
||||
return newclass
|
||||
attrs = {}
|
||||
if schema:
|
||||
attrs['schema'] = schema
|
||||
elif schema_file:
|
||||
attrs['schema_file'] = schema_file
|
||||
else:
|
||||
attrs['schema'] = name
|
||||
name = "".join((name[0].upper(), name[1:]))
|
||||
return BaseMeta(name, base_classes, attrs)
|
||||
|
||||
|
||||
def _add_from_schema(*args, **kwargs):
|
||||
generatedClass = from_schema(*args, **kwargs)
|
||||
def _add_class_from_schema(*args, **kwargs):
|
||||
generatedClass = _class_from_schema(*args, **kwargs)
|
||||
globals()[generatedClass.__name__] = generatedClass
|
||||
del generatedClass
|
||||
|
||||
@@ -330,55 +327,19 @@ for i in [
|
||||
'emotionModel',
|
||||
'emotionPlugin',
|
||||
'emotionSet',
|
||||
'entry',
|
||||
'entity',
|
||||
'help',
|
||||
'plugin',
|
||||
'plugins',
|
||||
'response',
|
||||
'results',
|
||||
'sentiment',
|
||||
'sentimentPlugin',
|
||||
'suggestion',
|
||||
'aggregatedEvaluation',
|
||||
'evaluation',
|
||||
'metric',
|
||||
'dataset',
|
||||
'datasets',
|
||||
|
||||
]:
|
||||
_add_from_schema(i)
|
||||
|
||||
_ErrorModel = from_schema('error')
|
||||
|
||||
|
||||
class Error(SenpyMixin, Exception):
|
||||
def __init__(self, message, *args, **kwargs):
|
||||
super(Error, self).__init__(self, message, message)
|
||||
self._error = _ErrorModel(message=message, *args, **kwargs)
|
||||
self.message = message
|
||||
|
||||
def validate(self, obj=None):
|
||||
self._error.validate()
|
||||
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.to_JSON(with_context=False))
|
||||
|
||||
|
||||
register(Error, 'error')
|
||||
_add_class_from_schema(i)
|
||||
|
@@ -1,13 +1,17 @@
|
||||
from future import standard_library
|
||||
standard_library.install_aliases()
|
||||
|
||||
|
||||
from future.utils import with_metaclass
|
||||
|
||||
import os.path
|
||||
import os
|
||||
import re
|
||||
import pickle
|
||||
import logging
|
||||
import copy
|
||||
import pprint
|
||||
|
||||
import fnmatch
|
||||
import inspect
|
||||
import sys
|
||||
import subprocess
|
||||
@@ -15,30 +19,93 @@ import importlib
|
||||
import yaml
|
||||
import threading
|
||||
|
||||
from contextlib import contextmanager
|
||||
import numpy as np
|
||||
|
||||
from .. import models, utils
|
||||
from ..api import API_PARAMS
|
||||
from .. import api
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from gsitk.evaluation.evaluation import Evaluation as Eval
|
||||
from sklearn.pipeline import Pipeline
|
||||
GSITK_AVAILABLE = True
|
||||
except ImportError:
|
||||
logger.warn('GSITK is not installed. Some functions will be unavailable.')
|
||||
GSITK_AVAILABLE = False
|
||||
|
||||
class Plugin(models.Plugin):
|
||||
def __init__(self, info=None, data_folder=None):
|
||||
|
||||
class PluginMeta(models.BaseMeta):
|
||||
_classes = {}
|
||||
|
||||
def __new__(mcs, name, bases, attrs, **kwargs):
|
||||
plugin_type = []
|
||||
if hasattr(bases[0], 'plugin_type'):
|
||||
plugin_type += bases[0].plugin_type
|
||||
plugin_type.append(name)
|
||||
alias = attrs.get('name', name)
|
||||
attrs['plugin_type'] = plugin_type
|
||||
attrs['name'] = alias
|
||||
if 'description' not in attrs:
|
||||
doc = attrs.get('__doc__', None)
|
||||
if not doc:
|
||||
raise Exception(('Please, add a description or '
|
||||
'documentation to class {}').format(name))
|
||||
attrs['description'] = doc
|
||||
attrs['name'] = alias
|
||||
cls = super(PluginMeta, mcs).__new__(mcs, name, bases, attrs)
|
||||
|
||||
if alias in mcs._classes:
|
||||
if os.environ.get('SENPY_TESTING', ""):
|
||||
raise Exception(('The type of plugin {} already exists. '
|
||||
'Please, choose a different name').format(name))
|
||||
else:
|
||||
logger.warn('Overloading plugin class: {}'.format(alias))
|
||||
mcs._classes[alias] = cls
|
||||
return cls
|
||||
|
||||
@classmethod
|
||||
def for_type(cls, ptype):
|
||||
return cls._classes[ptype]
|
||||
|
||||
|
||||
class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
||||
'''
|
||||
Base class for all plugins in senpy.
|
||||
A plugin must provide at least these attributes:
|
||||
|
||||
- version
|
||||
- description (or docstring)
|
||||
- author
|
||||
|
||||
Additionally, they may provide a URL (url) of a repository or website.
|
||||
|
||||
'''
|
||||
|
||||
def __init__(self, info=None, data_folder=None, **kwargs):
|
||||
"""
|
||||
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(Plugin, self).__init__(id=id, **info)
|
||||
super(Plugin, self).__init__(**kwargs)
|
||||
if info:
|
||||
self.update(info)
|
||||
self.validate()
|
||||
self.id = 'plugins/{}_{}'.format(self['name'], self['version'])
|
||||
self.is_activated = False
|
||||
self._lock = threading.Lock()
|
||||
self.data_folder = data_folder or os.getcwd()
|
||||
|
||||
def validate(self):
|
||||
missing = []
|
||||
for x in ['name', 'description', 'version']:
|
||||
if x not in self:
|
||||
missing.append(x)
|
||||
if missing:
|
||||
raise models.Error('Missing configuration parameters: {}'.format(missing))
|
||||
|
||||
def get_folder(self):
|
||||
return os.path.dirname(inspect.getfile(self.__class__))
|
||||
|
||||
@@ -48,37 +115,62 @@ class Plugin(models.Plugin):
|
||||
def deactivate(self):
|
||||
pass
|
||||
|
||||
def test(self):
|
||||
def test(self, test_cases=None):
|
||||
if not test_cases:
|
||||
if not hasattr(self, 'test_cases'):
|
||||
import inspect
|
||||
raise AttributeError(('Plugin {} [{}] does not have any defined '
|
||||
'test cases').format(self.id, inspect.getfile(self.__class__)))
|
||||
for case in self.test_cases:
|
||||
res = list(self.analyse_entry(models.Entry(case['entry']),
|
||||
case['params']))
|
||||
exp = case['expected']
|
||||
if not isinstance(exp, list):
|
||||
exp = [exp]
|
||||
utils.check_template(res, exp)
|
||||
'test cases').format(self.id,
|
||||
inspect.getfile(self.__class__)))
|
||||
test_cases = self.test_cases
|
||||
for case in test_cases:
|
||||
try:
|
||||
self.test_case(case)
|
||||
logger.debug('Test case passed:\n{}'.format(pprint.pformat(case)))
|
||||
except Exception as ex:
|
||||
logger.warn('Test case failed:\n{}'.format(pprint.pformat(case)))
|
||||
raise
|
||||
|
||||
def test_case(self, case):
|
||||
entry = models.Entry(case['entry'])
|
||||
given_parameters = case.get('params', case.get('parameters', {}))
|
||||
expected = case.get('expected', None)
|
||||
should_fail = case.get('should_fail', False)
|
||||
try:
|
||||
params = api.parse_params(given_parameters, self.extra_params)
|
||||
res = list(self.analyse_entries([entry, ], params))
|
||||
|
||||
if not isinstance(expected, list):
|
||||
expected = [expected]
|
||||
utils.check_template(res, expected)
|
||||
for r in res:
|
||||
r.validate()
|
||||
except models.Error:
|
||||
if should_fail:
|
||||
return
|
||||
raise
|
||||
assert not should_fail
|
||||
|
||||
@contextmanager
|
||||
def open(self, fpath, *args, **kwargs):
|
||||
if not os.path.isabs(fpath):
|
||||
fpath = os.path.join(self.data_folder, fpath)
|
||||
with open(fpath, *args, **kwargs) as f:
|
||||
yield f
|
||||
return open(fpath, *args, **kwargs)
|
||||
|
||||
def serve(self, debug=True, **kwargs):
|
||||
utils.easy(plugin_list=[self, ], plugin_folder=None, debug=debug, **kwargs)
|
||||
|
||||
|
||||
# For backwards compatibility
|
||||
SenpyPlugin = Plugin
|
||||
|
||||
|
||||
class AnalysisPlugin(Plugin):
|
||||
class Analysis(Plugin):
|
||||
'''
|
||||
A subclass of Plugin that analyses text and provides an annotation.
|
||||
'''
|
||||
|
||||
def analyse(self, *args, **kwargs):
|
||||
raise NotImplemented(
|
||||
'Your method should implement either analyse or analyse_entry')
|
||||
raise NotImplementedError(
|
||||
'Your plugin should implement either analyse or analyse_entry')
|
||||
|
||||
def analyse_entry(self, entry, parameters):
|
||||
""" An implemented plugin should override this method.
|
||||
@@ -97,41 +189,193 @@ class AnalysisPlugin(Plugin):
|
||||
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):
|
||||
results = self.analyse_entry(entry, parameters)
|
||||
if inspect.isgenerator(results):
|
||||
for result in results:
|
||||
yield result
|
||||
else:
|
||||
yield results
|
||||
|
||||
def test_case(self, case):
|
||||
if 'entry' not in case and 'input' in case:
|
||||
entry = models.Entry(_auto_id=False)
|
||||
entry.nif__isString = case['input']
|
||||
case['entry'] = entry
|
||||
super(Analysis, self).test_case(case)
|
||||
|
||||
|
||||
class ConversionPlugin(Plugin):
|
||||
AnalysisPlugin = Analysis
|
||||
|
||||
|
||||
class Conversion(Plugin):
|
||||
'''
|
||||
A subclass of Plugins that convert between different annotation models.
|
||||
e.g. a conversion of emotion models, or normalization of sentiment values.
|
||||
'''
|
||||
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))
|
||||
ConversionPlugin = Conversion
|
||||
|
||||
|
||||
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 SentimentPlugin(Analysis, models.SentimentPlugin):
|
||||
'''
|
||||
Sentiment plugins provide sentiment annotation (using Marl)
|
||||
'''
|
||||
minPolarityValue = 0
|
||||
maxPolarityValue = 1
|
||||
|
||||
def test_case(self, case):
|
||||
if 'polarity' in case:
|
||||
expected = case.get('expected', {})
|
||||
s = models.Sentiment(_auto_id=False)
|
||||
s.marl__hasPolarity = case['polarity']
|
||||
if 'sentiments' not in expected:
|
||||
expected['sentiments'] = []
|
||||
expected['sentiments'].append(s)
|
||||
case['expected'] = expected
|
||||
super(SentimentPlugin, self).test_case(case)
|
||||
|
||||
|
||||
class EmotionConversionPlugin(models.EmotionConversionPlugin, ConversionPlugin):
|
||||
class EmotionPlugin(Analysis, models.EmotionPlugin):
|
||||
'''
|
||||
Emotion plugins provide emotion annotation (using Onyx)
|
||||
'''
|
||||
minEmotionValue = 0
|
||||
maxEmotionValue = 1
|
||||
|
||||
|
||||
class EmotionConversion(Conversion):
|
||||
'''
|
||||
A subclass of Conversion that converts emotion annotations using different models
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
EmotionConversionPlugin = EmotionConversion
|
||||
|
||||
|
||||
class Box(AnalysisPlugin):
|
||||
'''
|
||||
Black box plugins delegate analysis to a function.
|
||||
The flow is like so:
|
||||
|
||||
.. code-block::
|
||||
|
||||
entry --> input() --> predict_one() --> output() --> entry'
|
||||
|
||||
|
||||
In other words: their ``input`` method convers a query (entry and a set of parameters) into
|
||||
the input to the box method. The ``output`` method convers the results given by the box into
|
||||
an entry that senpy can handle.
|
||||
'''
|
||||
|
||||
def input(self, entry, params=None):
|
||||
'''Transforms a query (entry+param) into an input for the black box'''
|
||||
return entry
|
||||
|
||||
def output(self, output, entry=None, params=None):
|
||||
'''Transforms the results of the black box into an entry'''
|
||||
return output
|
||||
|
||||
def predict_one(self, input):
|
||||
raise NotImplementedError('You should define the behavior of this plugin')
|
||||
|
||||
def analyse_entries(self, entries, params):
|
||||
for entry in entries:
|
||||
input = self.input(entry=entry, params=params)
|
||||
results = self.predict_one(input=input)
|
||||
yield self.output(output=results, entry=entry, params=params)
|
||||
|
||||
def fit(self, X=None, y=None):
|
||||
return self
|
||||
|
||||
def transform(self, X):
|
||||
return np.array([self.predict_one(x) for x in X])
|
||||
|
||||
def predict(self, X):
|
||||
return self.transform(X)
|
||||
|
||||
def fit_transform(self, X, y):
|
||||
self.fit(X, y)
|
||||
return self.transform(X)
|
||||
|
||||
def as_pipe(self):
|
||||
pipe = Pipeline([('plugin', self)])
|
||||
pipe.name = self.name
|
||||
return pipe
|
||||
|
||||
|
||||
class TextBox(Box):
|
||||
'''A black box plugin that takes only text as input'''
|
||||
|
||||
def input(self, entry, params):
|
||||
entry = super(TextBox, self).input(entry, params)
|
||||
return entry['nif:isString']
|
||||
|
||||
|
||||
class SentimentBox(TextBox, SentimentPlugin):
|
||||
'''
|
||||
A box plugin where the output is only a polarity label or a tuple (polarity, polarityValue)
|
||||
'''
|
||||
|
||||
def output(self, output, entry, **kwargs):
|
||||
s = models.Sentiment()
|
||||
try:
|
||||
label, value = output
|
||||
except ValueError:
|
||||
label, value = output, None
|
||||
s.prov(self)
|
||||
s.polarity = label
|
||||
if value is not None:
|
||||
s.polarityValue = value
|
||||
entry.sentiments.append(s)
|
||||
return entry
|
||||
|
||||
|
||||
class EmotionBox(TextBox, EmotionPlugin):
|
||||
'''
|
||||
A box plugin where the output is only an a tuple of emotion labels
|
||||
'''
|
||||
|
||||
def output(self, output, entry, **kwargs):
|
||||
if not isinstance(output, list):
|
||||
output = [output]
|
||||
s = models.EmotionSet()
|
||||
entry.emotions.append(s)
|
||||
for label in output:
|
||||
e = models.Emotion(onyx__hasEmotionCategory=label)
|
||||
s.append(e)
|
||||
return entry
|
||||
|
||||
|
||||
class MappingMixin(object):
|
||||
|
||||
@property
|
||||
def mappings(self):
|
||||
return self._mappings
|
||||
|
||||
@mappings.setter
|
||||
def mappings(self, value):
|
||||
self._mappings = value
|
||||
|
||||
def output(self, output, entry, params):
|
||||
output = self.mappings.get(output,
|
||||
self.mappings.get('default', output))
|
||||
return super(MappingMixin, self).output(output=output,
|
||||
entry=entry,
|
||||
params=params)
|
||||
|
||||
|
||||
class ShelfMixin(object):
|
||||
@property
|
||||
def sh(self):
|
||||
if not hasattr(self, '_sh') or self._sh is None:
|
||||
self.__dict__['_sh'] = {}
|
||||
self._sh = {}
|
||||
if os.path.isfile(self.shelf_file):
|
||||
try:
|
||||
with self.open(self.shelf_file, 'rb') as p:
|
||||
self.__dict__['_sh'] = pickle.load(p)
|
||||
self._sh = pickle.load(p)
|
||||
except (IndexError, EOFError, pickle.UnpicklingError):
|
||||
logger.warning('{} has a corrupted shelf file!'.format(self.id))
|
||||
if not self.get('force_shelf', False):
|
||||
@@ -142,14 +386,22 @@ class ShelfMixin(object):
|
||||
def sh(self):
|
||||
if os.path.isfile(self.shelf_file):
|
||||
os.remove(self.shelf_file)
|
||||
del self.__dict__['_sh']
|
||||
del self._sh
|
||||
self.save()
|
||||
|
||||
@sh.setter
|
||||
def sh(self, value):
|
||||
self._sh = value
|
||||
|
||||
@property
|
||||
def shelf_file(self):
|
||||
if 'shelf_file' not in self or not self['shelf_file']:
|
||||
self.shelf_file = os.path.join(self.data_folder, self.name + '.p')
|
||||
return self['shelf_file']
|
||||
if not hasattr(self, '_shelf_file') or not self._shelf_file:
|
||||
self._shelf_file = os.path.join(self.data_folder, self.name + '.p')
|
||||
return self._shelf_file
|
||||
|
||||
@shelf_file.setter
|
||||
def shelf_file(self, value):
|
||||
self._shelf_file = value
|
||||
|
||||
def save(self):
|
||||
logger.debug('saving pickle')
|
||||
@@ -158,19 +410,18 @@ class ShelfMixin(object):
|
||||
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)
|
||||
ptype = kwargs.pop('plugin_type', Plugin)
|
||||
logger.debug('#' * 100)
|
||||
logger.debug('ptype {}'.format(ptype))
|
||||
if ptype:
|
||||
if isinstance(ptype, PluginMeta):
|
||||
ptype = ptype.__name__
|
||||
try:
|
||||
ptype = ptype[0].upper() + ptype[1:]
|
||||
pclass = globals()[ptype]
|
||||
@@ -192,11 +443,7 @@ def pfilter(plugins, **kwargs):
|
||||
|
||||
if kwargs:
|
||||
candidates = filter(matches, candidates)
|
||||
return {p.name: p for p in candidates}
|
||||
|
||||
|
||||
def validate_info(info):
|
||||
return all(x in info for x in ('name', 'module', 'description', 'version'))
|
||||
return candidates
|
||||
|
||||
|
||||
def load_module(name, root=None):
|
||||
@@ -208,7 +455,7 @@ def load_module(name, root=None):
|
||||
return tmp
|
||||
|
||||
|
||||
def log_subprocess_output(process):
|
||||
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''):
|
||||
@@ -220,14 +467,14 @@ def install_deps(*plugins):
|
||||
for info in plugins:
|
||||
requirements = info.get('requirements', [])
|
||||
if requirements:
|
||||
pip_args = [sys.executable, '-m', 'pip', 'install', '--use-wheel']
|
||||
pip_args = [sys.executable, '-m', 'pip', 'install']
|
||||
for req in requirements:
|
||||
pip_args.append(req)
|
||||
logger.info('Installing requirements: ' + str(requirements))
|
||||
process = subprocess.Popen(pip_args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
log_subprocess_output(process)
|
||||
_log_subprocess_output(process)
|
||||
exitcode = process.wait()
|
||||
installed = True
|
||||
if exitcode != 0:
|
||||
@@ -235,59 +482,157 @@ def install_deps(*plugins):
|
||||
return installed
|
||||
|
||||
|
||||
def load_plugin_from_info(info, root=None, validator=validate_info, install=True, *args, **kwargs):
|
||||
if not root and '_path' in info:
|
||||
root = os.path.dirname(info['_path'])
|
||||
if not validator(info):
|
||||
raise ValueError('Plugin info is not valid: {}'.format(info))
|
||||
module = info["module"]
|
||||
|
||||
try:
|
||||
tmp = load_module(module, root)
|
||||
except ImportError:
|
||||
if not install:
|
||||
raise
|
||||
install_deps(info)
|
||||
tmp = load_module(module, root)
|
||||
candidate = None
|
||||
for _, obj in inspect.getmembers(tmp):
|
||||
if inspect.isclass(obj) and inspect.getmodule(obj) == tmp:
|
||||
logger.debug(("Found plugin class:"
|
||||
" {}@{}").format(obj, inspect.getmodule(obj)))
|
||||
candidate = obj
|
||||
break
|
||||
if not candidate:
|
||||
logger.debug("No valid plugin for: {}".format(module))
|
||||
return
|
||||
module = candidate(info=info, *args, **kwargs)
|
||||
return module
|
||||
is_plugin_file = re.compile(r'.*\.senpy$|senpy_[a-zA-Z0-9_]+\.py$|'
|
||||
'^(?!test_)[a-zA-Z0-9_]+_plugin.py$')
|
||||
|
||||
|
||||
def parse_plugin_info(fpath):
|
||||
logger.debug("Loading plugin: {}".format(fpath))
|
||||
with open(fpath, 'r') as f:
|
||||
info = yaml.load(f)
|
||||
info['_path'] = fpath
|
||||
name = info['name']
|
||||
return name, info
|
||||
|
||||
|
||||
def load_plugin(fpath, *args, **kwargs):
|
||||
name, info = parse_plugin_info(fpath)
|
||||
logger.debug("Info: {}".format(info))
|
||||
plugin = load_plugin_from_info(info, *args, **kwargs)
|
||||
return name, plugin
|
||||
|
||||
|
||||
def load_plugins(folders, loader=load_plugin, *args, **kwargs):
|
||||
plugins = {}
|
||||
def find_plugins(folders):
|
||||
for search_folder in folders:
|
||||
for root, dirnames, filenames in os.walk(search_folder):
|
||||
# Do not look for plugins in hidden or special folders
|
||||
dirnames[:] = [d for d in dirnames if d[0] not in ['.', '_']]
|
||||
for filename in fnmatch.filter(filenames, '*.senpy'):
|
||||
for filename in filter(is_plugin_file.match, filenames):
|
||||
fpath = os.path.join(root, filename)
|
||||
name, plugin = loader(fpath, *args, **kwargs)
|
||||
if plugin and name:
|
||||
plugins[name] = plugin
|
||||
yield fpath
|
||||
|
||||
|
||||
def from_path(fpath, **kwargs):
|
||||
logger.debug("Loading plugin from {}".format(fpath))
|
||||
if fpath.endswith('.py'):
|
||||
# We asume root is the dir of the file, and module is the name of the file
|
||||
root = os.path.dirname(fpath)
|
||||
module = os.path.basename(fpath)[:-3]
|
||||
for instance in _from_module_name(module=module, root=root, **kwargs):
|
||||
yield instance
|
||||
else:
|
||||
info = parse_plugin_info(fpath)
|
||||
yield from_info(info, **kwargs)
|
||||
|
||||
|
||||
def from_folder(folders, loader=from_path, **kwargs):
|
||||
plugins = []
|
||||
for fpath in find_plugins(folders):
|
||||
for plugin in loader(fpath, **kwargs):
|
||||
plugins.append(plugin)
|
||||
return plugins
|
||||
|
||||
|
||||
def from_info(info, root=None, **kwargs):
|
||||
if any(x not in info for x in ('module',)):
|
||||
raise ValueError('Plugin info is not valid: {}'.format(info))
|
||||
module = info["module"]
|
||||
|
||||
if not root and '_path' in info:
|
||||
root = os.path.dirname(info['_path'])
|
||||
|
||||
return one_from_module(module, root=root, info=info, **kwargs)
|
||||
|
||||
|
||||
def parse_plugin_info(fpath):
|
||||
logger.debug("Parsing plugin info: {}".format(fpath))
|
||||
with open(fpath, 'r') as f:
|
||||
info = yaml.load(f)
|
||||
info['_path'] = fpath
|
||||
return info
|
||||
|
||||
|
||||
def from_module(module, **kwargs):
|
||||
|
||||
if inspect.ismodule(module):
|
||||
res = _from_loaded_module(module, **kwargs)
|
||||
else:
|
||||
res = _from_module_name(module, **kwargs)
|
||||
for p in res:
|
||||
yield p
|
||||
|
||||
|
||||
def one_from_module(module, root, info, **kwargs):
|
||||
if '@type' in info:
|
||||
cls = PluginMeta.from_type(info['@type'])
|
||||
return cls(info=info, **kwargs)
|
||||
instance = next(from_module(module=module, root=root, info=info, **kwargs), None)
|
||||
if not instance:
|
||||
raise Exception("No valid plugin for: {}".format(module))
|
||||
return instance
|
||||
|
||||
|
||||
def _classes_in_module(module):
|
||||
for _, obj in inspect.getmembers(module):
|
||||
if inspect.isclass(obj) and inspect.getmodule(obj) == module:
|
||||
logger.debug(("Found plugin class:"
|
||||
" {}@{}").format(obj, inspect.getmodule(obj)))
|
||||
yield obj
|
||||
|
||||
|
||||
def _instances_in_module(module):
|
||||
for _, obj in inspect.getmembers(module):
|
||||
if isinstance(obj, Plugin) and inspect.getmodule(obj) == module:
|
||||
logger.debug(("Found plugin instance:"
|
||||
" {}@{}").format(obj, inspect.getmodule(obj)))
|
||||
yield obj
|
||||
|
||||
|
||||
def _from_module_name(module, root, info=None, install=True, **kwargs):
|
||||
try:
|
||||
module = load_module(module, root)
|
||||
except ImportError:
|
||||
if not install or not info:
|
||||
raise
|
||||
install_deps(info)
|
||||
module = load_module(module, root)
|
||||
for plugin in _from_loaded_module(module=module, root=root, info=info, **kwargs):
|
||||
yield plugin
|
||||
|
||||
|
||||
def _from_loaded_module(module, info=None, **kwargs):
|
||||
for cls in _classes_in_module(module):
|
||||
yield cls(info=info, **kwargs)
|
||||
for instance in _instances_in_module(module):
|
||||
yield instance
|
||||
|
||||
|
||||
def evaluate(plugins, datasets, **kwargs):
|
||||
if not GSITK_AVAILABLE:
|
||||
raise Exception('GSITK is not available. Install it to use this function.')
|
||||
|
||||
ev = Eval(tuples=None,
|
||||
datasets=datasets,
|
||||
pipelines=[plugin.as_pipe() for plugin in plugins])
|
||||
ev.evaluate()
|
||||
results = ev.results
|
||||
evaluations = evaluations_to_JSONLD(results, **kwargs)
|
||||
return evaluations
|
||||
|
||||
|
||||
def evaluations_to_JSONLD(results, flatten=False):
|
||||
'''
|
||||
Map the evaluation results to a JSONLD scheme
|
||||
'''
|
||||
|
||||
evaluations = list()
|
||||
metric_names = ['accuracy', 'precision_macro', 'recall_macro',
|
||||
'f1_macro', 'f1_weighted', 'f1_micro', 'f1_macro']
|
||||
|
||||
for index, row in results.iterrows():
|
||||
evaluation = models.Evaluation()
|
||||
if row.get('CV', True):
|
||||
evaluation['@type'] = ['StaticCV', 'Evaluation']
|
||||
evaluation.evaluatesOn = row['Dataset']
|
||||
evaluation.evaluates = row['Model']
|
||||
i = 0
|
||||
if flatten:
|
||||
metric = models.Metric()
|
||||
for name in metric_names:
|
||||
metric[name] = row[name]
|
||||
evaluation.metrics.append(metric)
|
||||
else:
|
||||
# We should probably discontinue this representation
|
||||
for name in metric_names:
|
||||
metric = models.Metric()
|
||||
metric['@id'] = 'Metric' + str(i)
|
||||
metric['@type'] = name.capitalize()
|
||||
metric.value = row[name]
|
||||
evaluation.metrics.append(metric)
|
||||
i += 1
|
||||
evaluations.append(evaluation)
|
||||
return evaluations
|
||||
|
@@ -6,6 +6,11 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CentroidConversion(EmotionConversionPlugin):
|
||||
'''
|
||||
This plugin converts emotion annotations from a dimensional model to a
|
||||
categorical one, and vice versa. The centroids used in the conversion
|
||||
are configurable and appear in the semantic description of the plugin.
|
||||
'''
|
||||
def __init__(self, info, *args, **kwargs):
|
||||
if 'centroids' not in info:
|
||||
raise Error('Centroid conversion plugins should provide '
|
||||
|
@@ -4,7 +4,15 @@ from senpy.plugins import EmotionPlugin
|
||||
from senpy.models import EmotionSet, Emotion, Entry
|
||||
|
||||
|
||||
class RmoRandPlugin(EmotionPlugin):
|
||||
class EmoRand(EmotionPlugin):
|
||||
name = "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"
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
category = "emoml:big6happiness"
|
||||
number = max(-1, min(1, random.gauss(0, 0.5)))
|
||||
|
@@ -1,9 +0,0 @@
|
||||
---
|
||||
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"
|
32
senpy/plugins/example/emorand_plugin.py
Normal file
32
senpy/plugins/example/emorand_plugin.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import random
|
||||
|
||||
from senpy.plugins import EmotionPlugin
|
||||
from senpy.models import EmotionSet, Emotion, Entry
|
||||
|
||||
|
||||
class EmoRand(EmotionPlugin):
|
||||
'''A sample plugin that returns a random emotion annotation'''
|
||||
author = '@balkian'
|
||||
version = '0.1'
|
||||
url = "https://github.com/gsi-upm/senpy-plugins-community"
|
||||
onyx__usesEmotionModel = "emoml:big6"
|
||||
|
||||
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
|
||||
|
||||
def test(self):
|
||||
params = dict()
|
||||
results = list()
|
||||
for i in range(100):
|
||||
res = next(self.analyse_entry(Entry(nif__isString="Hello"), params))
|
||||
res.validate()
|
||||
results.append(res.emotions[0]['onyx:hasEmotion'][0]['onyx:hasEmotionCategory'])
|
@@ -1,10 +0,0 @@
|
||||
---
|
||||
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"
|
@@ -1,33 +1,35 @@
|
||||
import random
|
||||
|
||||
from senpy.plugins import SentimentPlugin
|
||||
from senpy.models import Sentiment, Entry
|
||||
from senpy import SentimentPlugin, Sentiment, Entry
|
||||
|
||||
|
||||
class RandPlugin(SentimentPlugin):
|
||||
class Rand(SentimentPlugin):
|
||||
'''A sample plugin that returns a random sentiment annotation'''
|
||||
author = "@balkian"
|
||||
version = '0.1'
|
||||
url = "https://github.com/gsi-upm/senpy-plugins-community"
|
||||
marl__maxPolarityValue = '1'
|
||||
marl__minPolarityValue = "-1"
|
||||
|
||||
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
|
||||
sentiment = Sentiment(marl__hasPolarity=polarity,
|
||||
marl__polarityValue=polarity_value)
|
||||
sentiment.prov(self)
|
||||
entry.sentiments.append(sentiment)
|
||||
entry.language = lang
|
||||
yield entry
|
||||
|
||||
def test(self):
|
||||
'''Run several random analyses.'''
|
||||
params = dict()
|
||||
results = list()
|
||||
for i in range(100):
|
||||
res = next(self.analyse_entry(Entry(nif__isString="Hello"), params))
|
||||
for i in range(50):
|
||||
res = next(self.analyse_entry(Entry(nif__isString="Hello"),
|
||||
params))
|
||||
res.validate()
|
||||
results.append(res.sentiments[0]['marl:hasPolarity'])
|
||||
assert 'marl:Positive' in results
|
@@ -6,12 +6,13 @@ import nltk
|
||||
|
||||
|
||||
class SplitPlugin(AnalysisPlugin):
|
||||
'''description: A sample plugin that chunks input text'''
|
||||
|
||||
def activate(self):
|
||||
nltk.download('punkt')
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
chunker_type = params.get("delimiter", "sentence")
|
||||
chunker_type = params["delimiter"]
|
||||
original_text = entry['nif:isString']
|
||||
if chunker_type == "sentence":
|
||||
tokenizer = PunktSentenceTokenizer()
|
||||
@@ -45,7 +46,7 @@ class SplitPlugin(AnalysisPlugin):
|
||||
},
|
||||
{
|
||||
'entry': {
|
||||
"id": ":test",
|
||||
"@id": ":test",
|
||||
'nif:isString': 'Hello\nWorld'
|
||||
},
|
||||
'params': {
|
||||
|
@@ -6,8 +6,9 @@ from senpy.models import Sentiment
|
||||
|
||||
|
||||
class Sentiment140Plugin(SentimentPlugin):
|
||||
'''Connects to the sentiment140 free API: http://sentiment140.com'''
|
||||
def analyse_entry(self, entry, params):
|
||||
lang = params.get("language", "auto")
|
||||
lang = params["language"]
|
||||
res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson",
|
||||
json.dumps({
|
||||
"language": lang,
|
||||
@@ -35,6 +36,18 @@ class Sentiment140Plugin(SentimentPlugin):
|
||||
entry.language = lang
|
||||
yield entry
|
||||
|
||||
def test(self, *args, **kwargs):
|
||||
'''
|
||||
To avoid calling the sentiment140 API, we will mock the results
|
||||
from requests.
|
||||
'''
|
||||
from senpy.test import patch_requests
|
||||
expected = {"data": [{"polarity": 4}]}
|
||||
with patch_requests(expected) as (request, response):
|
||||
super(Sentiment140Plugin, self).test(*args, **kwargs)
|
||||
assert request.called
|
||||
assert response.json.called
|
||||
|
||||
test_cases = [
|
||||
{
|
||||
'entry': {
|
||||
|
@@ -16,6 +16,7 @@ extra_params:
|
||||
- es
|
||||
- en
|
||||
- auto
|
||||
default: auto
|
||||
requirements: {}
|
||||
maxPolarityValue: 1
|
||||
minPolarityValue: 0
|
38
senpy/schemas/aggregatedEvaluation.json
Normal file
38
senpy/schemas/aggregatedEvaluation.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"allOf": [
|
||||
{"$ref": "response.json"},
|
||||
{
|
||||
"title": "AggregatedEvaluation",
|
||||
"description": "The results of the evaluation",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@context": {
|
||||
"$ref": "context.json"
|
||||
},
|
||||
"@type": {
|
||||
"default": "AggregatedEvaluation"
|
||||
},
|
||||
"@id": {
|
||||
"description": "ID of the aggregated evaluation",
|
||||
"type": "string"
|
||||
},
|
||||
"evaluations": {
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "evaluation.json"
|
||||
},{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
"required": ["@id", "evaluations"]
|
||||
}
|
||||
]
|
||||
}
|
29
senpy/schemas/dataset.json
Normal file
29
senpy/schemas/dataset.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"name": "Dataset",
|
||||
"properties": {
|
||||
"@id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"compression": {
|
||||
"type": "string"
|
||||
},
|
||||
"expected_bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"filename": {
|
||||
"description": "Name of the dataset",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "Classifier or plugin evaluated",
|
||||
"type": "string"
|
||||
},
|
||||
"stats": {
|
||||
}
|
||||
},
|
||||
"required": ["@id"]
|
||||
}
|
18
senpy/schemas/datasets.json
Normal file
18
senpy/schemas/datasets.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"allOf": [
|
||||
{"$ref": "response.json"},
|
||||
{
|
||||
"required": ["datasets"],
|
||||
"properties": {
|
||||
"datasets": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "dataset.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@@ -41,5 +41,20 @@
|
||||
},
|
||||
"Response": {
|
||||
"$ref": "response.json"
|
||||
},
|
||||
"AggregatedEvaluation": {
|
||||
"$ref": "aggregatedEvaluation.json"
|
||||
},
|
||||
"Evaluation": {
|
||||
"$ref": "evaluation.json"
|
||||
},
|
||||
"Metric": {
|
||||
"$ref": "metric.json"
|
||||
},
|
||||
"Dataset": {
|
||||
"$ref": "dataset.json"
|
||||
},
|
||||
"Datasets": {
|
||||
"$ref": "datasets.json"
|
||||
}
|
||||
}
|
||||
|
28
senpy/schemas/evaluation.json
Normal file
28
senpy/schemas/evaluation.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"name": "Evalation",
|
||||
"properties": {
|
||||
"@id": {
|
||||
"type": "string"
|
||||
},
|
||||
"@type": {
|
||||
"type": "array",
|
||||
"default": "Evaluation"
|
||||
|
||||
},
|
||||
"metrics": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "metric.json" },
|
||||
"default": []
|
||||
},
|
||||
"evaluatesOn": {
|
||||
"description": "Name of the dataset evaluated ",
|
||||
"type": "string"
|
||||
},
|
||||
"evaluates": {
|
||||
"description": "Classifier or plugin evaluated",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["@id", "metrics"]
|
||||
}
|
@@ -7,11 +7,11 @@
|
||||
"description": "Help containing accepted parameters",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"parameters": {
|
||||
"valid_parameters": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": "parameters"
|
||||
"required": "valid_parameters"
|
||||
}
|
||||
]
|
||||
}
|
24
senpy/schemas/metric.json
Normal file
24
senpy/schemas/metric.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"properties": {
|
||||
"@id": {
|
||||
"type": "string"
|
||||
},
|
||||
"@type": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxValue": {
|
||||
"type": "number"
|
||||
},
|
||||
"minValue": {
|
||||
"type": "number"
|
||||
},
|
||||
"value": {
|
||||
"type": "number"
|
||||
},
|
||||
"deviation": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["@id"]
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"type": "object",
|
||||
"required": ["@id", "extra_params"],
|
||||
"required": ["@id", "name", "description", "version", "plugin_type"],
|
||||
"properties": {
|
||||
"@id": {
|
||||
"type": "string",
|
||||
@@ -9,7 +9,19 @@
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the plugin, which will be used in the algorithm detection phase"
|
||||
"description": "The name of the plugin, which will be used in the algorithm detection phase."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A summary of what the plugin does, and pointers to further information."
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "The version of the plugin."
|
||||
},
|
||||
"plugin_type": {
|
||||
"type": "string",
|
||||
"description": "Sub-type of plugin. e.g. sentimentPlugin"
|
||||
},
|
||||
"extra_params": {
|
||||
"type": "object",
|
||||
|
@@ -152,3 +152,18 @@ textarea{
|
||||
/* background: white; */
|
||||
display: none;
|
||||
}
|
||||
|
||||
.deco-none {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
.deco-none:link {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
.deco-none:hover {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
@@ -1,7 +1,10 @@
|
||||
var ONYX = "http://www.gsi.dit.upm.es/ontologies/onyx/ns#";
|
||||
var RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
|
||||
var plugins_params={};
|
||||
var default_params = JSON.parse($.ajax({type: "GET", url: "/api?help=true" , async: false}).responseText);
|
||||
var plugins_params = default_params = {};
|
||||
var plugins = [];
|
||||
var defaultPlugin = {};
|
||||
var gplugins = {};
|
||||
|
||||
function replaceURLWithHTMLLinks(text) {
|
||||
console.log('Text: ' + text);
|
||||
var exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
|
||||
@@ -25,21 +28,38 @@ function hashchanged(){
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
var response = JSON.parse($.ajax({type: "GET", url: "/api/plugins/" , async: false}).responseText);
|
||||
var defaultPlugin= JSON.parse($.ajax({type: "GET", url: "/api/plugins/default" , async: false}).responseText);
|
||||
html="";
|
||||
var availablePlugins = document.getElementById('availablePlugins');
|
||||
|
||||
function get_plugins(response){
|
||||
plugins = response.plugins;
|
||||
gplugins = {};
|
||||
}
|
||||
|
||||
function get_datasets(response){
|
||||
datasets = response.datasets
|
||||
}
|
||||
|
||||
function group_plugins(){
|
||||
for (r in plugins){
|
||||
ptype = plugins[r]['@type'];
|
||||
if(gplugins[ptype] == undefined){
|
||||
gplugins[ptype] = [r]
|
||||
gplugins[ptype] = [r];
|
||||
}else{
|
||||
gplugins[ptype].push(r)
|
||||
gplugins[ptype].push(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function get_parameters(){
|
||||
for (p in plugins){
|
||||
plugin = plugins[p];
|
||||
if (plugin["extra_params"]){
|
||||
plugins_params[plugin["name"]] = plugin["extra_params"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function draw_plugins_selection(){
|
||||
html="";
|
||||
group_plugins();
|
||||
for (g in gplugins){
|
||||
html += "<optgroup label=\""+g+"\">"
|
||||
for (r in gplugins[g]){
|
||||
@@ -49,7 +69,7 @@ $(document).ready(function() {
|
||||
continue;
|
||||
|
||||
}
|
||||
html+= "<option value=\""+plugin["name"]+"\" "
|
||||
html+= "<option value=\""+plugin.name+"\" "
|
||||
if (plugin["name"] == defaultPlugin["name"]){
|
||||
html+= " selected=\"selected\""
|
||||
}
|
||||
@@ -57,24 +77,22 @@ $(document).ready(function() {
|
||||
html+= " disabled=\"disabled\" "
|
||||
}
|
||||
html+=">"+plugin["name"]+"</option>"
|
||||
|
||||
}
|
||||
}
|
||||
html += "</optgroup>"
|
||||
// Two elements with plugin class
|
||||
// One from the evaluate tab and another one from the analyse tab
|
||||
document.getElementsByClassName('plugin')[0].innerHTML = html;
|
||||
document.getElementsByClassName('plugin')[1].innerHTML = html;
|
||||
}
|
||||
|
||||
if (plugin["extra_params"]){
|
||||
plugins_params[plugin["name"]]={};
|
||||
for (param in plugin["extra_params"]){
|
||||
if (typeof plugin["extra_params"][param] !="string"){
|
||||
var params = new Array();
|
||||
var alias = plugin["extra_params"][param]["aliases"][0];
|
||||
params[alias]=new Array();
|
||||
for (option in plugin["extra_params"][param]["options"]){
|
||||
params[alias].push(plugin["extra_params"][param]["options"][option])
|
||||
}
|
||||
plugins_params[plugin["name"]][alias] = (params[alias])
|
||||
}
|
||||
}
|
||||
}
|
||||
function draw_plugins_list(){
|
||||
var availablePlugins = document.getElementById('availablePlugins');
|
||||
|
||||
for(p in plugins){
|
||||
var pluginEntry = document.createElement('li');
|
||||
|
||||
plugin = plugins[p];
|
||||
newHtml = ""
|
||||
if(plugin.url) {
|
||||
newHtml= "<a href="+plugin.url+">" + plugin.name + "</a>";
|
||||
@@ -85,9 +103,31 @@ $(document).ready(function() {
|
||||
pluginEntry.innerHTML = newHtml;
|
||||
availablePlugins.appendChild(pluginEntry)
|
||||
}
|
||||
html += "</optgroup>"
|
||||
document.getElementById('plugins').innerHTML = html;
|
||||
change_params();
|
||||
}
|
||||
|
||||
function draw_datasets(){
|
||||
html = "";
|
||||
repeated_html = "<input class=\"checks-datasets\" type=\"checkbox\" value=\"";
|
||||
for (dataset in datasets){
|
||||
html += repeated_html+datasets[dataset]["@id"]+"\">"+datasets[dataset]["@id"];
|
||||
html += "<br>"
|
||||
}
|
||||
document.getElementById("datasets").innerHTML = html;
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
var response = JSON.parse($.ajax({type: "GET", url: "/api/plugins/" , async: false}).responseText);
|
||||
defaultPlugin= JSON.parse($.ajax({type: "GET", url: "/api/plugins/default" , async: false}).responseText);
|
||||
var response2 = JSON.parse($.ajax({type: "GET", url: "/api/datasets/" , async: false}).responseText);
|
||||
|
||||
get_plugins(response);
|
||||
get_default_parameters();
|
||||
get_datasets(response2);
|
||||
|
||||
draw_plugins_list();
|
||||
draw_plugins_selection();
|
||||
draw_parameters();
|
||||
draw_datasets();
|
||||
|
||||
$(window).on('hashchange', hashchanged);
|
||||
hashchanged();
|
||||
@@ -95,49 +135,132 @@ $(document).ready(function() {
|
||||
|
||||
});
|
||||
|
||||
|
||||
function change_params(){
|
||||
var plugin = document.getElementById("plugins").options[document.getElementById("plugins").selectedIndex].value;
|
||||
html=""
|
||||
for (param in default_params){
|
||||
if ((default_params[param]['options']) && (['help','conversion'].indexOf(param) < 0)){
|
||||
html+= "<label> "+param+"</label>"
|
||||
if (default_params[param]['options'].length < 1) {
|
||||
html +="<input></input>";
|
||||
}
|
||||
else {
|
||||
html+= "<select id=\""+param+"\" name=\""+param+"\">"
|
||||
for (option in default_params[param]['options']){
|
||||
if (default_params[param]['options'][option] == default_params[param]['default']){
|
||||
html+="<option value \""+default_params[param]['options'][option]+"\" selected >"+default_params[param]['options'][option]+"</option>"
|
||||
}
|
||||
else{
|
||||
html+="<option value \""+default_params[param]['options'][option]+"\">"+default_params[param]['options'][option]+"</option>"
|
||||
function get_default_parameters(){
|
||||
default_params = JSON.parse($.ajax({type: "GET", url: "/api?help=true" , async: false}).responseText).valid_parameters;
|
||||
// Remove the parameters that are always added
|
||||
delete default_params["input"];
|
||||
delete default_params["algorithm"];
|
||||
delete default_params["help"];
|
||||
|
||||
}
|
||||
|
||||
function draw_default_parameters(){
|
||||
var basic_params = document.getElementById("basic_params");
|
||||
basic_params.innerHTML = params_div(default_params);
|
||||
}
|
||||
|
||||
function draw_extra_parameters(){
|
||||
var plugin = document.getElementsByClassName('plugin')[0].options[document.getElementsByClassName('plugin')[0].selectedIndex].value;
|
||||
get_parameters();
|
||||
|
||||
var extra_params = document.getElementById("extra_params");
|
||||
extra_params.innerHTML = params_div(plugins_params[plugin]);
|
||||
}
|
||||
html+="</select><br>"
|
||||
|
||||
function draw_parameters(){
|
||||
draw_default_parameters();
|
||||
draw_extra_parameters();
|
||||
}
|
||||
|
||||
|
||||
function add_default_params(){
|
||||
var html = "";
|
||||
// html += '<a href="#basic_params" class="btn btn-info" data-toggle="collapse">Basic API parameters</a>';
|
||||
html += '<span id="basic_params" class="panel-collapse collapse">';
|
||||
html += '<ul class="list-group">'
|
||||
html += params_div(default_params);
|
||||
html += '</span>';
|
||||
return html;
|
||||
}
|
||||
for (param in plugins_params[plugin]){
|
||||
if (param || plugins_params[plugin][param].length > 1){
|
||||
html+= "<label> Parameter "+param+"</label>"
|
||||
param_opts = plugins_params[plugin][param]
|
||||
if (param_opts.length > 0) {
|
||||
html+= "<select id=\""+param+"\" name=\""+param+"\">"
|
||||
for (option in param_opts){
|
||||
html+="<option value \""+param_opts[option]+"\">"+param_opts[option]+"</option>"
|
||||
|
||||
function params_div(params){
|
||||
var html = '<div class="container-fluid">';
|
||||
if (Object.keys(params).length === 0) {
|
||||
html += '<p class="text text-muted text-center">This plugin does not take any extra parameters</p>';
|
||||
}
|
||||
// Iterate over the keys in order
|
||||
pnames = Object.keys(params).sort()
|
||||
for (ix in pnames){
|
||||
pname = pnames[ix];
|
||||
param = params[pname];
|
||||
html+='<div class="form-group">';
|
||||
html += '<div class="row">'
|
||||
html+= '<label class="col-sm-4" for="'+pname+'">'+pname+'</label>'
|
||||
if (param.options){
|
||||
opts = param.options;
|
||||
if(param.options.length == 1 && param.options[0] == 'boolean') {
|
||||
opts = [true, false];
|
||||
}
|
||||
html+= '<select class="col-sm-8" id="'+pname+"\" name=\""+pname+"\">"
|
||||
var defaultopt = param.default;
|
||||
for (option in opts){
|
||||
isselected = "";
|
||||
if (defaultopt != undefined && opts[option] == defaultopt ){
|
||||
isselected = ' selected="selected"'
|
||||
}
|
||||
html+="<option value=\""+opts[option]+'"' + isselected +
|
||||
'>'+opts[option]+"</option>"
|
||||
}
|
||||
html+="</select>"
|
||||
}
|
||||
else {
|
||||
html +="<input id=\""+param+"\" name=\""+param+"\"></input>";
|
||||
}
|
||||
}
|
||||
}
|
||||
document.getElementById("params").innerHTML = html
|
||||
default_value = "";
|
||||
if(param.default != undefined){
|
||||
default_value = param.default;
|
||||
};
|
||||
html +='<input class="col-sm-8" id="'+pname+'" name="'+pname+'" value="' + default_value + '"></input>';
|
||||
}
|
||||
html+='</div>';
|
||||
html+='<div class="row">';
|
||||
if ('description' in param){
|
||||
html += '<p class="form-text sm-sm-12 text-muted text-center">' + param.description + '</p>';
|
||||
|
||||
}
|
||||
html+='</div>';
|
||||
html+='</div>';
|
||||
}
|
||||
html+='</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function _get_form_parameters(id){
|
||||
var element = document.getElementById(id);
|
||||
params = {};
|
||||
var selects = element.getElementsByTagName('select');
|
||||
var inputs = element.getElementsByTagName('input');
|
||||
|
||||
Array.prototype.forEach.call(selects, function (sel) {
|
||||
key = sel.name;
|
||||
value = sel.options[sel.selectedIndex].value
|
||||
params[key] = value;
|
||||
});
|
||||
|
||||
Array.prototype.forEach.call(inputs, function (el) {
|
||||
params[el.name] = el.value;
|
||||
});
|
||||
|
||||
for (k in params){
|
||||
value = params[k];
|
||||
if (value == "" || value === "undefined"){
|
||||
delete params[k];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function get_form_parameters(){
|
||||
var p1 = _get_form_parameters("basic_params");
|
||||
var p2 = _get_form_parameters("extra_params");
|
||||
return Object.assign(p1, p2);
|
||||
}
|
||||
|
||||
function add_param(key, value){
|
||||
value = encodeURIComponent(value);
|
||||
return "&"+key+"="+value;
|
||||
}
|
||||
|
||||
|
||||
function load_JSON(){
|
||||
url = "/api";
|
||||
@@ -145,34 +268,20 @@ function load_JSON(){
|
||||
var rawcontainer = document.getElementById("jsonraw");
|
||||
rawcontainer.innerHTML = '';
|
||||
container.innerHTML = '';
|
||||
var plugin = document.getElementById("plugins").options[document.getElementById("plugins").selectedIndex].value;
|
||||
|
||||
var plugin = document.getElementsByClassName("plugin")[0].options[document.getElementsByClassName("plugin")[0].selectedIndex].value;
|
||||
|
||||
var input = encodeURIComponent(document.getElementById("input").value);
|
||||
url += "?algo="+plugin+"&i="+input
|
||||
for (param in plugins_params[plugin]){
|
||||
if (param != null){
|
||||
field = document.getElementById(param);
|
||||
if (plugins_params[plugin][param].length > 0){
|
||||
var param_value = encodeURIComponent(field.options[field.selectedIndex].text);
|
||||
} else {
|
||||
var param_value = encodeURIComponent(field.text);
|
||||
}
|
||||
if (param_value !== "undefined" && param_value.length > 0){
|
||||
url+="&"+param+"="+param_value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (param in default_params){
|
||||
if ((param != null) && (default_params[param]['options']) && (['help','conversion'].indexOf(param) < 0)){
|
||||
var param_value = encodeURIComponent(document.getElementById(param).options[document.getElementById(param).selectedIndex].value);
|
||||
if (param_value){
|
||||
url+="&"+param+"="+param_value
|
||||
}
|
||||
}
|
||||
params = get_form_parameters();
|
||||
|
||||
for (key in params){
|
||||
url += add_param(key, params[key]);
|
||||
}
|
||||
|
||||
var response = $.ajax({type: "GET", url: url , async: false}).responseText;
|
||||
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response)
|
||||
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
|
||||
|
||||
document.getElementById("input_request").innerHTML = "<a href='"+url+"'>"+url+"</a>"
|
||||
document.getElementById("results-div").style.display = 'block';
|
||||
@@ -183,12 +292,95 @@ function load_JSON(){
|
||||
};
|
||||
var editor = new JSONEditor(container, options, response);
|
||||
editor.expandAll();
|
||||
// $('#results-div a[href="#viewer"]').tab('show');
|
||||
$('#results-div a[href="#viewer"]').click();
|
||||
// location.hash = 'raw';
|
||||
}
|
||||
catch(err){
|
||||
console.log("Error decoding JSON (got turtle?)");
|
||||
$('#results-div a[href="#raw"]').click();
|
||||
// location.hash = 'raw';
|
||||
}
|
||||
}
|
||||
|
||||
function get_datasets_from_checkbox(){
|
||||
var checks = document.getElementsByClassName("checks-datasets");
|
||||
|
||||
datasets = "";
|
||||
for (var i = 0; i < checks.length; i++){
|
||||
if (checks[i].checked){
|
||||
datasets += checks[i].value + ",";
|
||||
}
|
||||
}
|
||||
datasets = datasets.slice(0, -1);
|
||||
}
|
||||
|
||||
|
||||
function create_body_metrics(evaluations){
|
||||
var new_tbody = document.createElement('tbody')
|
||||
var metric_html = ""
|
||||
for (var eval in evaluations){
|
||||
metric_html += "<tr><th>"+evaluations[eval].evaluates+"</th><th>"+evaluations[eval].evaluatesOn+"</th>";
|
||||
for (var metric in evaluations[eval].metrics){
|
||||
metric_html += "<th>"+parseFloat(evaluations[eval].metrics[metric].value.toFixed(4))+"</th>";
|
||||
}
|
||||
metric_html += "</tr>";
|
||||
}
|
||||
new_tbody.innerHTML = metric_html
|
||||
return new_tbody
|
||||
}
|
||||
|
||||
function evaluate_JSON(){
|
||||
|
||||
url = "/api/evaluate";
|
||||
|
||||
var container = document.getElementById('results_eval');
|
||||
var rawcontainer = document.getElementById('jsonraw_eval');
|
||||
var table = document.getElementById("eval_table");
|
||||
|
||||
rawcontainer.innerHTML = "";
|
||||
container.innerHTML = "";
|
||||
|
||||
var plugin = document.getElementsByClassName("plugin")[0].options[document.getElementsByClassName("plugin")[0].selectedIndex].value;
|
||||
|
||||
get_datasets_from_checkbox();
|
||||
|
||||
url += "?algo="+plugin+"&dataset="+datasets
|
||||
|
||||
var response = $.ajax({type: "GET", url: url , async: false, dataType: 'json'}).responseText;
|
||||
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
|
||||
|
||||
document.getElementById("input_request_eval").innerHTML = "<a href='"+url+"'>"+url+"</a>"
|
||||
document.getElementById("evaluate-div").style.display = 'block';
|
||||
|
||||
try {
|
||||
response = JSON.parse(response);
|
||||
var options = {
|
||||
mode: 'view'
|
||||
};
|
||||
|
||||
//Control the single response results
|
||||
if (!(Array.isArray(response.evaluations))){
|
||||
response.evaluations = [response.evaluations]
|
||||
}
|
||||
|
||||
new_tbody = create_body_metrics(response.evaluations)
|
||||
table.replaceChild(new_tbody, table.lastElementChild)
|
||||
|
||||
var editor = new JSONEditor(container, options, response);
|
||||
editor.expandAll();
|
||||
// $('#results-div a[href="#viewer"]').tab('show');
|
||||
$('#evaluate-div a[href="#evaluate-table"]').click();
|
||||
// location.hash = 'raw';
|
||||
|
||||
|
||||
}
|
||||
catch(err){
|
||||
console.log("Error decoding JSON (got turtle?)");
|
||||
$('#evaluate-div a[href="#evaluate-raw"]').click();
|
||||
// location.hash = 'raw';
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
@@ -32,6 +32,8 @@
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" ><a class="active" href="#about">About</a></li>
|
||||
<li role="presentation"class="active"><a class="active" href="#test">Test it</a></li>
|
||||
<li role="presentation"><a class="active" href="#evaluate">Evaluate Plugins</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
@@ -54,6 +56,7 @@
|
||||
<ul>
|
||||
<li>List all available plugins: <a href="/api/plugins">/api/plugins</a></li>
|
||||
<li>Get information about the default plugin: <a href="/api/plugins/default">/api/plugins/default</a></li>
|
||||
<li>List all available datasets: <a href="/api/datasets">/api/datasets</a></li>
|
||||
<li>Download the JSON-LD context used: <a href="/api/contexts/Results.jsonld">/api/contexts/Results.jsonld</a></li>
|
||||
</ul>
|
||||
|
||||
@@ -88,28 +91,52 @@
|
||||
|
||||
<div class="tab-pane active" id="test">
|
||||
<div class="well">
|
||||
<form id="form" onsubmit="return getPlugins();" accept-charset="utf-8">
|
||||
<div id="inputswrapper">
|
||||
<form id="form" class="container" onsubmit="return getPlugins();" accept-charset="utf-8">
|
||||
<div><textarea id="input" class="boxsizingBorder" rows="5" name="i">This text makes me sad.
|
||||
whilst this text makes me happy and surprised at the same time.
|
||||
I cannot believe it!</textarea></div>
|
||||
<label>Select the plugin:</label>
|
||||
<select id="plugins" name="plugins" onchange="change_params()">
|
||||
</select>
|
||||
</br>
|
||||
<div id ="params">
|
||||
I cannot believe it!</textarea>
|
||||
</div>
|
||||
</br>
|
||||
<div>
|
||||
<label>Select the plugin:</label>
|
||||
<select id="plugins" name="plugins" class=plugin onchange="draw_extra_parameters()">
|
||||
</select>
|
||||
</div>
|
||||
<!-- PARAMETERS -->
|
||||
<div class="panel-group" id="parameters">
|
||||
<div class="panel panel-default">
|
||||
<a data-toggle="collapse" class="deco-none" href="#basic_params">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
Basic API parameters
|
||||
</h4>
|
||||
</div>
|
||||
</a>
|
||||
<div id="basic_params" class="panel-collapse collapse panel-body">
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<a data-toggle="collapse" class="deco-none" href="#extra_params">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
Plugin extra parameters
|
||||
</h4>
|
||||
</div>
|
||||
</a>
|
||||
<div id="extra_params" class="panel-collapse collapse in panel-body">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END PARAMETERS -->
|
||||
|
||||
<a id="preview" class="btn btn-lg btn-primary" onclick="load_JSON()">Analyse!</a>
|
||||
<!--<button id="visualise" name="type" type="button">Visualise!</button>-->
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<span id="input_request"></span>
|
||||
<div id="results-div">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a class="active" href="#viewer">Viewer</a></li>
|
||||
<li role="presentation"><a class="active" href="#raw">Raw</a></li>
|
||||
<li role="presentation" class="active"><a data-toggle="tab" class="active" href="#viewer">Viewer</a></li>
|
||||
<li role="presentation"><a data-toggle="tab" class="active" href="#raw">Raw</a></li>
|
||||
</ul>
|
||||
<div class="tab-content" id="results-container">
|
||||
|
||||
@@ -127,6 +154,70 @@ I cannot believe it!</textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="evaluate">
|
||||
<div class="well">
|
||||
<form id="form" class="container" onsubmit="return getPlugins();" accept-charset="utf-8">
|
||||
<div>
|
||||
<label>Select the plugin:</label>
|
||||
<select id="plugins-eval" name="plugins-eval" class=plugin onchange="draw_extra_parameters()">
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Select the datasets:</label>
|
||||
<div id="datasets" name="datasets" >
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<a id="preview" class="btn btn-lg btn-primary" onclick="evaluate_JSON()">Evaluate Plugin!</a>
|
||||
<!--<button id="visualise" name="type" type="button">Visualise!</button>-->
|
||||
</form>
|
||||
</div>
|
||||
<span id="input_request_eval"></span>
|
||||
<div id="evaluate-div">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a data-toggle="tab" class="active" href="#evaluate-viewer">Viewer</a></li>
|
||||
<li role="presentation"><a data-toggle="tab" class="active" href="#evaluate-raw">Raw</a></li>
|
||||
<li role="presentation"><a data-toggle="tab" class="active" href="#evaluate-table">Table</a></li>
|
||||
</ul>
|
||||
<div class="tab-content" id="evaluate-container">
|
||||
|
||||
<div class="tab-pane active" id="evaluate-viewer">
|
||||
<div id="content">
|
||||
<pre id="results_eval" class="results_eval"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="evaluate-raw">
|
||||
<div id="content">
|
||||
<pre id="jsonraw_eval" class="results_eval"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="evaluate-table">
|
||||
<table id="eval_table" class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Plugin</th>
|
||||
<th>Dataset</th>
|
||||
<th>Accuracy</th>
|
||||
<th>Precision_macro</th>
|
||||
<th>Recall_macro</th>
|
||||
<th>F1_macro</th>
|
||||
<th>F1_weighted</th>
|
||||
<th>F1_micro</th>
|
||||
<th>F1</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="http://www.gsi.dit.upm.es" target="_blank"><img class="center-block" src="static/img/gsi.png"/> </a>
|
||||
|
||||
</div>
|
||||
|
36
senpy/test.py
Normal file
36
senpy/test.py
Normal file
@@ -0,0 +1,36 @@
|
||||
try:
|
||||
from unittest.mock import patch, MagicMock
|
||||
except ImportError:
|
||||
from mock import patch, MagicMock
|
||||
|
||||
from past.builtins import basestring
|
||||
|
||||
|
||||
import json
|
||||
from contextlib import contextmanager
|
||||
|
||||
from .models import BaseModel
|
||||
|
||||
|
||||
@contextmanager
|
||||
def patch_requests(value, code=200):
|
||||
success = MagicMock()
|
||||
if isinstance(value, BaseModel):
|
||||
value = value.jsonld()
|
||||
if not isinstance(value, basestring):
|
||||
data = json.dumps(value)
|
||||
else:
|
||||
data = value
|
||||
|
||||
success.json.return_value = value
|
||||
|
||||
success.status_code = code
|
||||
success.content = data
|
||||
success.text = data
|
||||
|
||||
method_mocker = MagicMock()
|
||||
method_mocker.return_value = success
|
||||
with patch.multiple('requests', request=method_mocker,
|
||||
get=method_mocker, post=method_mocker):
|
||||
yield method_mocker, success
|
||||
assert method_mocker.called
|
@@ -1,25 +1,101 @@
|
||||
from . import models
|
||||
from . import models, __version__
|
||||
from collections import MutableMapping
|
||||
import pprint
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# MutableMapping should be enough, but it causes problems with py2
|
||||
DICTCLASSES = (MutableMapping, dict, models.BaseModel)
|
||||
|
||||
|
||||
def check_template(indict, template):
|
||||
if isinstance(template, dict) and isinstance(indict, dict):
|
||||
if isinstance(template, DICTCLASSES) and isinstance(indict, DICTCLASSES):
|
||||
for k, v in template.items():
|
||||
if k not in indict:
|
||||
return '{} not in {}'.format(k, indict)
|
||||
raise models.Error('{} not in {}'.format(k, indict))
|
||||
check_template(indict[k], v)
|
||||
elif isinstance(template, list) and isinstance(indict, list):
|
||||
if len(indict) != len(template):
|
||||
raise models.Error('Different size for {} and {}'.format(indict, template))
|
||||
for e in template:
|
||||
found = False
|
||||
for i in indict:
|
||||
try:
|
||||
check_template(i, e)
|
||||
found = True
|
||||
break
|
||||
except models.Error as ex:
|
||||
# raise
|
||||
continue
|
||||
if not found:
|
||||
raise models.Error('{} not found in {}'.format(e, indict))
|
||||
else:
|
||||
raise models.Error(('Element not found.'
|
||||
'\nExpected: {}\nIn: {}').format(pprint.pformat(e),
|
||||
pprint.pformat(indict)))
|
||||
else:
|
||||
if indict != template:
|
||||
raise models.Error('{} and {} are different'.format(indict, template))
|
||||
raise models.Error(('Differences found.\n'
|
||||
'\tExpected: {}\n'
|
||||
'\tFound: {}').format(pprint.pformat(indict),
|
||||
pprint.pformat(template)))
|
||||
|
||||
|
||||
def convert_dictionary(original, mappings):
|
||||
result = {}
|
||||
for key, value in original.items():
|
||||
if key in mappings:
|
||||
key = mappings[key]
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def easy_load(app=None, plugin_list=None, plugin_folder=None, **kwargs):
|
||||
'''
|
||||
Run a server with a specific plugin.
|
||||
'''
|
||||
|
||||
from flask import Flask
|
||||
from .extensions import Senpy
|
||||
|
||||
if not app:
|
||||
app = Flask(__name__)
|
||||
sp = Senpy(app, plugin_folder=plugin_folder, **kwargs)
|
||||
if not plugin_list:
|
||||
from . import plugins
|
||||
import __main__
|
||||
plugin_list = plugins.from_module(__main__)
|
||||
for plugin in plugin_list:
|
||||
sp.add_plugin(plugin)
|
||||
sp.install_deps()
|
||||
sp.activate_all()
|
||||
return sp, app
|
||||
|
||||
|
||||
def easy_test(plugin_list=None):
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
if not plugin_list:
|
||||
import __main__
|
||||
logger.info('Loading classes from {}'.format(__main__))
|
||||
from . import plugins
|
||||
plugin_list = plugins.from_module(__main__)
|
||||
for plug in plugin_list:
|
||||
plug.test()
|
||||
logger.info('The tests for {} passed!'.format(plug.name))
|
||||
logger.info('All tests passed!')
|
||||
|
||||
|
||||
def easy(host='0.0.0.0', port=5000, debug=True, **kwargs):
|
||||
'''
|
||||
Run a server with a specific plugin.
|
||||
'''
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
logging.getLogger('senpy').setLevel(logging.INFO)
|
||||
sp, app = easy_load(**kwargs)
|
||||
easy_test(sp.plugins())
|
||||
app.debug = debug
|
||||
import time
|
||||
logger.info(time.time())
|
||||
logger.info('Senpy version {}'.format(__version__))
|
||||
logger.info('Server running on port %s:%d. Ctrl+C to quit' % (host,
|
||||
port))
|
||||
app.debug = debug
|
||||
app.run(host,
|
||||
port,
|
||||
debug=app.debug)
|
||||
|
29
setup.py
29
setup.py
@@ -1,20 +1,20 @@
|
||||
import pip
|
||||
from setuptools import setup
|
||||
# parse_requirements() returns generator of pip.req.InstallRequirement objects
|
||||
from pip.req import parse_requirements
|
||||
from senpy import __version__
|
||||
|
||||
try:
|
||||
install_reqs = parse_requirements(
|
||||
"requirements.txt", session=pip.download.PipSession())
|
||||
test_reqs = parse_requirements(
|
||||
"test-requirements.txt", session=pip.download.PipSession())
|
||||
except AttributeError:
|
||||
with open('senpy/VERSION') as f:
|
||||
__version__ = f.read().strip()
|
||||
assert __version__
|
||||
|
||||
|
||||
def parse_requirements(filename):
|
||||
""" load requirements from a pip requirements file """
|
||||
with open(filename, 'r') as f:
|
||||
lineiter = list(line.strip() for line in f)
|
||||
return [line for line in lineiter if line and not line.startswith("#")]
|
||||
|
||||
|
||||
install_reqs = parse_requirements("requirements.txt")
|
||||
test_reqs = parse_requirements("test-requirements.txt")
|
||||
|
||||
install_reqs = [str(ir.req) for ir in install_reqs]
|
||||
test_reqs = [str(ir.req) for ir in test_reqs]
|
||||
extra_reqs = parse_requirements("extra-requirements.txt")
|
||||
|
||||
|
||||
setup(
|
||||
@@ -34,6 +34,9 @@ setup(
|
||||
install_requires=install_reqs,
|
||||
tests_require=test_reqs,
|
||||
setup_requires=['pytest-runner', ],
|
||||
extras_require={
|
||||
'evaluation': extra_reqs
|
||||
},
|
||||
include_package_data=True,
|
||||
entry_points={
|
||||
'console_scripts':
|
||||
|
@@ -1,3 +1,5 @@
|
||||
mock
|
||||
pytest-cov
|
||||
pytest
|
||||
scikit-learn
|
||||
numpy
|
||||
|
@@ -1,8 +0,0 @@
|
||||
---
|
||||
name: Async
|
||||
module: asyncplugin
|
||||
description: I am async
|
||||
author: "@balkian"
|
||||
version: '0.1'
|
||||
async: true
|
||||
extra_params: {}
|
@@ -1,11 +0,0 @@
|
||||
from senpy.plugins import SentimentPlugin
|
||||
|
||||
|
||||
class DummyPlugin(SentimentPlugin):
|
||||
def analyse_entry(self, entry, params):
|
||||
entry['nif:iString'] = entry['nif:isString'][::-1]
|
||||
entry.reversed = entry.get('reversed', 0) + 1
|
||||
yield entry
|
||||
|
||||
def test(self):
|
||||
pass
|
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "Dummy",
|
||||
"module": "dummy",
|
||||
"description": "I am dummy",
|
||||
"author": "@balkian",
|
||||
"version": "0.1",
|
||||
"extra_params": {
|
||||
"example": {
|
||||
"@id": "example_parameter",
|
||||
"aliases": ["example", "ex"],
|
||||
"required": false,
|
||||
"default": 0
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"name": "DummyRequired",
|
||||
"module": "dummy",
|
||||
"description": "I am dummy",
|
||||
"author": "@balkian",
|
||||
"version": "0.1",
|
||||
"extra_params": {
|
||||
"example": {
|
||||
"@id": "example_parameter",
|
||||
"aliases": ["example", "ex"],
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +0,0 @@
|
||||
from senpy.plugins import SentimentPlugin
|
||||
|
||||
|
||||
class DummyPlugin(SentimentPlugin):
|
||||
import noop
|
@@ -1,14 +0,0 @@
|
||||
from senpy.plugins import AnalysisPlugin
|
||||
from time import sleep
|
||||
|
||||
|
||||
class SleepPlugin(AnalysisPlugin):
|
||||
def activate(self, *args, **kwargs):
|
||||
sleep(self.timeout)
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
sleep(float(params.get("timeout", self.timeout)))
|
||||
yield entry
|
||||
|
||||
def test(self):
|
||||
pass
|
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"name": "Sleep",
|
||||
"module": "sleep",
|
||||
"description": "I am dummy",
|
||||
"author": "@balkian",
|
||||
"version": "0.1",
|
||||
"timeout": 0.05,
|
||||
"extra_params": {
|
||||
"timeout": {
|
||||
"@id": "timeout_sleep",
|
||||
"aliases": ["timeout", "to"],
|
||||
"required": false,
|
||||
"default": 0
|
||||
}
|
||||
}
|
||||
}
|
@@ -19,11 +19,8 @@ class APITest(TestCase):
|
||||
|
||||
def test_basic(self):
|
||||
a = {}
|
||||
try:
|
||||
parse_params(a, NIF_PARAMS)
|
||||
raise AssertionError()
|
||||
except Error:
|
||||
pass
|
||||
self.assertRaises(Error, parse_params, a)
|
||||
self.assertRaises(Error, parse_params, a, NIF_PARAMS)
|
||||
a = {'input': 'hello'}
|
||||
p = parse_params(a, NIF_PARAMS)
|
||||
assert 'input' in p
|
||||
@@ -39,11 +36,7 @@ class APITest(TestCase):
|
||||
'required': True
|
||||
}
|
||||
}
|
||||
try:
|
||||
parse_params(query, plug_params)
|
||||
raise AssertionError()
|
||||
except Error:
|
||||
pass
|
||||
self.assertRaises(Error, parse_params, plug_params)
|
||||
query['hello'] = 'world'
|
||||
p = parse_params(query, plug_params)
|
||||
assert 'hello' in p
|
||||
@@ -53,7 +46,6 @@ class APITest(TestCase):
|
||||
query['hiya'] = 'dlrow'
|
||||
p = parse_params(query, plug_params)
|
||||
assert 'hello' in p
|
||||
assert 'hiya' in p
|
||||
assert p['hello'] == 'dlrow'
|
||||
|
||||
def test_default(self):
|
||||
|
@@ -23,7 +23,7 @@ class BlueprintsTest(TestCase):
|
||||
cls.app = Flask("test_extensions")
|
||||
cls.app.debug = False
|
||||
cls.client = cls.app.test_client()
|
||||
cls.senpy = Senpy()
|
||||
cls.senpy = Senpy(default_plugins=True)
|
||||
cls.senpy.init_app(cls.app)
|
||||
cls.dir = os.path.join(os.path.dirname(__file__), "..")
|
||||
cls.senpy.add_folder(cls.dir)
|
||||
@@ -34,11 +34,14 @@ class BlueprintsTest(TestCase):
|
||||
def assertCode(self, resp, code):
|
||||
self.assertEqual(resp.status_code, code)
|
||||
|
||||
def test_playground(self):
|
||||
resp = self.client.get("/")
|
||||
assert "main.js" in resp.data.decode()
|
||||
|
||||
def test_home(self):
|
||||
"""
|
||||
Calling with no arguments should ask the user for more arguments
|
||||
"""
|
||||
self.app.debug = False
|
||||
resp = self.client.get("/api/")
|
||||
self.assertCode(resp, 400)
|
||||
js = parse_resp(resp)
|
||||
@@ -84,6 +87,10 @@ class BlueprintsTest(TestCase):
|
||||
js = parse_resp(resp)
|
||||
logging.debug("Got response: %s", js)
|
||||
assert isinstance(js, models.Error)
|
||||
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired&example=notvalid")
|
||||
self.assertCode(resp, 400)
|
||||
resp = self.client.get("/api/?i=My aloha mohame&algo=DummyRequired&example=a")
|
||||
self.assertCode(resp, 200)
|
||||
|
||||
def test_error(self):
|
||||
"""
|
||||
@@ -155,12 +162,15 @@ class BlueprintsTest(TestCase):
|
||||
def test_schema(self):
|
||||
resp = self.client.get("/api/schemas/definitions.json")
|
||||
self.assertCode(resp, 200)
|
||||
js = parse_resp(resp)
|
||||
assert "$schema" in js
|
||||
assert "$schema" in resp.data.decode()
|
||||
|
||||
def test_help(self):
|
||||
resp = self.client.get("/api/?help=true")
|
||||
self.assertCode(resp, 200)
|
||||
js = parse_resp(resp)
|
||||
assert "parameters" in js
|
||||
assert "help" in js["parameters"]
|
||||
assert "valid_parameters" in js
|
||||
assert "help" in js["valid_parameters"]
|
||||
|
||||
def test_conversion(self):
|
||||
resp = self.client.get("/api/?input=hello&algo=emoRand&emotionModel=DOES NOT EXIST")
|
||||
self.assertCode(resp, 404)
|
||||
|
@@ -12,7 +12,8 @@ class CLITest(TestCase):
|
||||
def test_basic(self):
|
||||
self.assertRaises(Error, partial(main_function, []))
|
||||
|
||||
res = main_function(['--input', 'test', '--algo', 'rand', '--with-parameters'])
|
||||
res = main_function(['--input', 'test', '--algo', 'rand',
|
||||
'--with-parameters', '--default-plugins'])
|
||||
assert res.parameters['input'] == 'test'
|
||||
assert 'rand' in res.parameters['algorithm']
|
||||
assert res.parameters['input'] == 'test'
|
||||
|
@@ -1,24 +1,9 @@
|
||||
from unittest import TestCase
|
||||
try:
|
||||
from unittest.mock import patch
|
||||
except ImportError:
|
||||
from mock import patch
|
||||
|
||||
import json
|
||||
|
||||
from senpy.test import patch_requests
|
||||
from senpy.client import Client
|
||||
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.serialize()
|
||||
self.status_code = 200
|
||||
self.content = self.json()
|
||||
|
||||
def json(self):
|
||||
return json.loads(self.obj)
|
||||
from senpy.plugins import AnalysisPlugin
|
||||
|
||||
|
||||
class ModelsTest(TestCase):
|
||||
@@ -29,20 +14,18 @@ class ModelsTest(TestCase):
|
||||
def test_client(self):
|
||||
endpoint = 'http://dummy/'
|
||||
client = Client(endpoint)
|
||||
success = Call(Results())
|
||||
with patch('requests.request', return_value=success) as patched:
|
||||
with patch_requests(Results()) as (request, response):
|
||||
resp = client.analyse('hello')
|
||||
assert isinstance(resp, Results)
|
||||
patched.assert_called_with(
|
||||
request.assert_called_with(
|
||||
url=endpoint + '/', method='GET', params={'input': 'hello'})
|
||||
error = Call(Error('Nothing'))
|
||||
with patch('requests.request', return_value=error) as patched:
|
||||
with patch_requests(Error('Nothing')) as (request, response):
|
||||
try:
|
||||
client.analyse(input='hello', algorithm='NONEXISTENT')
|
||||
raise Exception('Exceptions should be raised. This is not golang')
|
||||
except Error:
|
||||
pass
|
||||
patched.assert_called_with(
|
||||
request.assert_called_with(
|
||||
url=endpoint + '/',
|
||||
method='GET',
|
||||
params={'input': 'hello',
|
||||
@@ -54,12 +37,11 @@ class ModelsTest(TestCase):
|
||||
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:
|
||||
with patch_requests(plugins) as (request, response):
|
||||
response = client.plugins()
|
||||
assert isinstance(response, dict)
|
||||
assert len(response) == 1
|
||||
assert "AnalysisP1" in response
|
||||
patched.assert_called_with(
|
||||
request.assert_called_with(
|
||||
url=endpoint + '/plugins', method='GET',
|
||||
params={'plugin_type': default_plugin_type})
|
||||
params={})
|
||||
|
@@ -25,10 +25,11 @@ def analyse(instance, **kwargs):
|
||||
class ExtensionsTest(TestCase):
|
||||
def setUp(self):
|
||||
self.app = Flask('test_extensions')
|
||||
self.dir = os.path.dirname(__file__)
|
||||
self.senpy = Senpy(plugin_folder=self.dir,
|
||||
self.examples_dir = os.path.join(os.path.dirname(__file__), '..', 'example-plugins')
|
||||
self.senpy = Senpy(plugin_folder=self.examples_dir,
|
||||
app=self.app,
|
||||
default_plugins=False)
|
||||
self.senpy.deactivate_all()
|
||||
self.senpy.activate_plugin("Dummy", sync=True)
|
||||
|
||||
def test_init(self):
|
||||
@@ -41,21 +42,37 @@ class ExtensionsTest(TestCase):
|
||||
def test_discovery(self):
|
||||
""" Discovery of plugins in given folders. """
|
||||
# noinspection PyProtectedMember
|
||||
assert self.dir in self.senpy._search_folders
|
||||
print(self.senpy.plugins)
|
||||
assert "Dummy" in self.senpy.plugins
|
||||
print(self.senpy.plugins())
|
||||
assert self.senpy.get_plugin("dummy")
|
||||
|
||||
def test_add_delete(self):
|
||||
'''Should be able to add and delete new plugins. '''
|
||||
new = plugins.Plugin(name='new', description='new', version=0)
|
||||
self.senpy.add_plugin(new)
|
||||
assert new in self.senpy.plugins()
|
||||
self.senpy.delete_plugin(new)
|
||||
assert new not in self.senpy.plugins()
|
||||
|
||||
def test_adding_folder(self):
|
||||
""" It should be possible for senpy to look for plugins in more folders. """
|
||||
senpy = Senpy(plugin_folder=None,
|
||||
app=self.app,
|
||||
default_plugins=False)
|
||||
assert not senpy.analysis_plugins
|
||||
senpy.add_folder(self.examples_dir)
|
||||
assert senpy.analysis_plugins
|
||||
self.assertRaises(AttributeError, senpy.add_folder, 'DOES NOT EXIST')
|
||||
|
||||
def test_installing(self):
|
||||
""" Installing a plugin """
|
||||
info = {
|
||||
'name': 'TestPip',
|
||||
'module': 'noop_plugin',
|
||||
'module': 'mynoop',
|
||||
'description': None,
|
||||
'requirements': ['noop'],
|
||||
'version': 0
|
||||
}
|
||||
root = os.path.join(self.dir, 'plugins', 'noop')
|
||||
module = plugins.load_plugin_from_info(info, root=root, install=True)
|
||||
module = plugins.from_info(info, root=self.examples_dir, install=True)
|
||||
assert module.name == 'TestPip'
|
||||
assert module
|
||||
import noop
|
||||
@@ -64,8 +81,8 @@ class ExtensionsTest(TestCase):
|
||||
def test_enabling(self):
|
||||
""" Enabling a plugin """
|
||||
self.senpy.activate_all(sync=True)
|
||||
assert len(self.senpy.plugins) >= 3
|
||||
assert self.senpy.plugins["Sleep"].is_activated
|
||||
assert len(self.senpy.plugins()) >= 3
|
||||
assert self.senpy.get_plugin("Sleep").is_activated
|
||||
|
||||
def test_installing_nonexistent(self):
|
||||
""" Fail if the dependencies cannot be met """
|
||||
@@ -82,8 +99,8 @@ class ExtensionsTest(TestCase):
|
||||
def test_disabling(self):
|
||||
""" Disabling a plugin """
|
||||
self.senpy.deactivate_all(sync=True)
|
||||
assert not self.senpy.plugins["Dummy"].is_activated
|
||||
assert not self.senpy.plugins["Sleep"].is_activated
|
||||
assert not self.senpy.get_plugin("dummy").is_activated
|
||||
assert not self.senpy.get_plugin("sleep").is_activated
|
||||
|
||||
def test_default(self):
|
||||
""" Default plugin should be set """
|
||||
@@ -106,7 +123,18 @@ class ExtensionsTest(TestCase):
|
||||
r2 = analyse(self.senpy, input="tupni", output="tuptuo")
|
||||
assert r1.analysis[0] == "plugins/Dummy_0.1"
|
||||
assert r2.analysis[0] == "plugins/Dummy_0.1"
|
||||
assert r1.entries[0]['nif:iString'] == 'input'
|
||||
assert r1.entries[0]['nif:isString'] == 'input'
|
||||
|
||||
def test_analyse_empty(self):
|
||||
""" Trying to analyse when no plugins are installed should raise an error."""
|
||||
senpy = Senpy(plugin_folder=None,
|
||||
app=self.app,
|
||||
default_plugins=False)
|
||||
self.assertRaises(Error, senpy.analyse, Results())
|
||||
|
||||
def test_analyse_wrong(self):
|
||||
""" Trying to analyse with a non-existent plugin should raise an error."""
|
||||
self.assertRaises(Error, analyse, self.senpy, algorithm='DOES NOT EXIST', input='test')
|
||||
|
||||
def test_analyse_jsonld(self):
|
||||
""" Using a plugin with JSON-LD input"""
|
||||
@@ -130,14 +158,15 @@ class ExtensionsTest(TestCase):
|
||||
output="tuptuo")
|
||||
assert r1.analysis[0] == "plugins/Dummy_0.1"
|
||||
assert r2.analysis[0] == "plugins/Dummy_0.1"
|
||||
assert r1.entries[0]['nif:iString'] == 'input'
|
||||
assert r1.entries[0]['nif:isString'] == 'input'
|
||||
|
||||
def test_analyse_error(self):
|
||||
mm = mock.MagicMock()
|
||||
mm.id = 'magic_mock'
|
||||
mm.name = 'mock'
|
||||
mm.is_activated = True
|
||||
mm.analyse_entries.side_effect = Error('error in analysis', status=500)
|
||||
self.senpy.plugins['MOCK'] = mm
|
||||
self.senpy.add_plugin(mm)
|
||||
try:
|
||||
analyse(self.senpy, input='nothing', algorithm='MOCK')
|
||||
assert False
|
||||
@@ -145,29 +174,28 @@ class ExtensionsTest(TestCase):
|
||||
assert 'error in analysis' in ex['message']
|
||||
assert ex['status'] == 500
|
||||
|
||||
mm.analyse.side_effect = Exception('generic exception on analysis')
|
||||
mm.analyse_entries.side_effect = Exception(
|
||||
'generic exception on analysis')
|
||||
ex = Exception('generic exception on analysis')
|
||||
mm.analyse.side_effect = ex
|
||||
mm.analyse_entries.side_effect = ex
|
||||
|
||||
try:
|
||||
analyse(self.senpy, input='nothing', algorithm='MOCK')
|
||||
assert False
|
||||
except Error as ex:
|
||||
assert 'generic exception on analysis' in ex['message']
|
||||
assert ex['status'] == 500
|
||||
except Exception as ex:
|
||||
assert 'generic exception on analysis' in str(ex)
|
||||
|
||||
def test_filtering(self):
|
||||
""" Filtering plugins """
|
||||
assert len(self.senpy.filter_plugins(name="Dummy")) > 0
|
||||
assert not len(self.senpy.filter_plugins(name="notdummy"))
|
||||
assert self.senpy.filter_plugins(name="Dummy", is_activated=True)
|
||||
assert len(self.senpy.plugins(name="Dummy")) > 0
|
||||
assert not len(self.senpy.plugins(name="NotDummy"))
|
||||
assert self.senpy.plugins(name="Dummy", is_activated=True)
|
||||
self.senpy.deactivate_plugin("Dummy", sync=True)
|
||||
assert not len(
|
||||
self.senpy.filter_plugins(name="Dummy", is_activated=True))
|
||||
assert not len(self.senpy.plugins(name="Dummy",
|
||||
is_activated=True))
|
||||
|
||||
def test_load_default_plugins(self):
|
||||
senpy = Senpy(plugin_folder=self.dir, default_plugins=True)
|
||||
assert len(senpy.plugins) > 1
|
||||
senpy = Senpy(plugin_folder=self.examples_dir, default_plugins=True)
|
||||
assert len(senpy.plugins()) > 1
|
||||
|
||||
def test_convert_emotions(self):
|
||||
self.senpy.activate_all(sync=True)
|
||||
@@ -185,7 +213,7 @@ class ExtensionsTest(TestCase):
|
||||
response = Results({
|
||||
'analysis': [{'plugin': plugin}],
|
||||
'entries': [Entry({
|
||||
'nif:iString': 'much ado about nothing',
|
||||
'nif:isString': 'much ado about nothing',
|
||||
'emotions': [eSet1]
|
||||
})]
|
||||
})
|
||||
|
@@ -9,13 +9,15 @@ from senpy.models import (Emotion,
|
||||
EmotionAnalysis,
|
||||
EmotionSet,
|
||||
Entry,
|
||||
Entity,
|
||||
Error,
|
||||
Results,
|
||||
Sentiment,
|
||||
SentimentPlugin,
|
||||
Plugins,
|
||||
Plugin,
|
||||
from_string,
|
||||
from_dict)
|
||||
from_dict,
|
||||
subtypes)
|
||||
from senpy import plugins
|
||||
from pprint import pprint
|
||||
|
||||
@@ -99,7 +101,8 @@ class ModelsTest(TestCase):
|
||||
|
||||
def test_plugins(self):
|
||||
self.assertRaises(Error, plugins.Plugin)
|
||||
p = plugins.Plugin({"name": "dummy",
|
||||
p = plugins.SentimentPlugin({"name": "dummy",
|
||||
"description": "I do nothing",
|
||||
"version": 0,
|
||||
"extra_params": {
|
||||
"none": {
|
||||
@@ -110,7 +113,7 @@ class ModelsTest(TestCase):
|
||||
}})
|
||||
c = p.jsonld()
|
||||
assert '@type' in c
|
||||
assert c['@type'] == 'plugin'
|
||||
assert c['@type'] == 'sentimentPlugin'
|
||||
assert 'info' not in c
|
||||
assert 'repo' not in c
|
||||
assert 'extra_params' in c
|
||||
@@ -123,7 +126,9 @@ class ModelsTest(TestCase):
|
||||
def test_str(self):
|
||||
"""The string representation shouldn't include private variables"""
|
||||
r = Results()
|
||||
p = plugins.Plugin({"name": "STR test", "version": 0})
|
||||
p = plugins.Plugin({"name": "STR test",
|
||||
"description": "Test of private variables.",
|
||||
"version": 0})
|
||||
p._testing = 0
|
||||
s = str(p)
|
||||
assert "_testing" not in s
|
||||
@@ -131,6 +136,11 @@ class ModelsTest(TestCase):
|
||||
s = str(r)
|
||||
assert "_testing" not in s
|
||||
|
||||
def test_serialize(self):
|
||||
for k, v in subtypes().items():
|
||||
e = v()
|
||||
e.serialize()
|
||||
|
||||
def test_turtle(self):
|
||||
"""Any model should be serializable as a turtle file"""
|
||||
ana = EmotionAnalysis()
|
||||
@@ -170,13 +180,14 @@ class ModelsTest(TestCase):
|
||||
def test_single_plugin(self):
|
||||
"""A response with a single plugin should still return a list"""
|
||||
plugs = Plugins()
|
||||
p = Plugin({'id': str(1),
|
||||
p = SentimentPlugin({'id': str(1),
|
||||
'version': 0,
|
||||
'description': 'dummy'})
|
||||
plugs.plugins.append(p)
|
||||
assert isinstance(plugs.plugins, list)
|
||||
js = plugs.jsonld()
|
||||
assert isinstance(js['plugins'], list)
|
||||
assert js['plugins'][0]['@type'] == 'sentimentPlugin'
|
||||
|
||||
def test_from_string(self):
|
||||
results = {
|
||||
@@ -189,6 +200,7 @@ class ModelsTest(TestCase):
|
||||
}]
|
||||
}
|
||||
recovered = from_dict(results)
|
||||
assert recovered.id == results['@id']
|
||||
assert isinstance(recovered, Results)
|
||||
assert isinstance(recovered.entries[0], Entry)
|
||||
|
||||
@@ -196,3 +208,14 @@ class ModelsTest(TestCase):
|
||||
recovered = from_string(string)
|
||||
assert isinstance(recovered, Results)
|
||||
assert isinstance(recovered.entries[0], Entry)
|
||||
|
||||
def test_serializable(self):
|
||||
r = Results()
|
||||
e = Entry()
|
||||
ent = Entity()
|
||||
e.entities.append(ent)
|
||||
r.entries.append(e)
|
||||
d = r.serializable()
|
||||
assert d
|
||||
assert d['entries']
|
||||
assert d['entries'][0]['entities']
|
||||
|
@@ -1,17 +1,25 @@
|
||||
#!/bin/env python
|
||||
|
||||
import os
|
||||
import sys
|
||||
import pickle
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from unittest import TestCase
|
||||
from senpy.models import Results, Entry, EmotionSet, Emotion
|
||||
from unittest import TestCase, skipIf
|
||||
from senpy.models import Results, Entry, EmotionSet, Emotion, Plugins
|
||||
from senpy import plugins
|
||||
from senpy.plugins.conversion.emotion.centroids import CentroidConversion
|
||||
|
||||
import pandas as pd
|
||||
|
||||
|
||||
class ShelfDummyPlugin(plugins.SentimentPlugin, plugins.ShelfMixin):
|
||||
'''Dummy plugin for tests.'''
|
||||
name = 'Shelf'
|
||||
version = 0
|
||||
author = 'the senpy community'
|
||||
|
||||
def activate(self, *args, **kwargs):
|
||||
if 'counter' not in self.sh:
|
||||
self.sh['counter'] = 0
|
||||
@@ -40,19 +48,50 @@ class PluginsTest(TestCase):
|
||||
self.shelf_dir = tempfile.mkdtemp()
|
||||
self.shelf_file = os.path.join(self.shelf_dir, "shelf")
|
||||
|
||||
def test_serialize(self):
|
||||
'''A plugin should be serializable and de-serializable'''
|
||||
dummy = ShelfDummyPlugin()
|
||||
dummy.serialize()
|
||||
|
||||
def test_jsonld(self):
|
||||
'''A plugin should be serializable and de-serializable'''
|
||||
dummy = ShelfDummyPlugin()
|
||||
dummy.jsonld()
|
||||
|
||||
def test_shelf_file(self):
|
||||
a = ShelfDummyPlugin(
|
||||
info={'name': 'default_shelve_file',
|
||||
'description': 'Dummy plugin for tests',
|
||||
'version': 'test'})
|
||||
a.activate()
|
||||
assert os.path.isfile(a.shelf_file)
|
||||
os.remove(a.shelf_file)
|
||||
|
||||
def test_plugin_filter(self):
|
||||
ps = Plugins()
|
||||
for i in (plugins.SentimentPlugin,
|
||||
plugins.EmotionPlugin,
|
||||
plugins.AnalysisPlugin):
|
||||
p = i(name='Plugin_{}'.format(i.__name__),
|
||||
description='TEST',
|
||||
version=0,
|
||||
author='NOBODY')
|
||||
ps.plugins.append(p)
|
||||
assert len(ps.plugins) == 3
|
||||
cases = [('AnalysisPlugin', 3),
|
||||
('SentimentPlugin', 1),
|
||||
('EmotionPlugin', 1)]
|
||||
|
||||
for name, num in cases:
|
||||
res = list(plugins.pfilter(ps.plugins, plugin_type=name))
|
||||
assert len(res) == num
|
||||
|
||||
def test_shelf(self):
|
||||
''' A shelf is created and the value is stored '''
|
||||
newfile = self.shelf_file + "new"
|
||||
a = ShelfDummyPlugin(info={
|
||||
'name': 'shelve',
|
||||
'description': 'Shelf plugin for tests',
|
||||
'version': 'test',
|
||||
'shelf_file': newfile
|
||||
})
|
||||
@@ -75,6 +114,7 @@ class PluginsTest(TestCase):
|
||||
pickle.dump({'counter': 99}, f)
|
||||
a = ShelfDummyPlugin(info={
|
||||
'name': 'DummyShelf',
|
||||
'description': 'Dummy plugin for tests',
|
||||
'shelf_file': self.shelf_file,
|
||||
'version': 'test'
|
||||
})
|
||||
@@ -105,7 +145,8 @@ class PluginsTest(TestCase):
|
||||
with open(fn, 'rb') as f:
|
||||
msg, error = files[fn]
|
||||
a = ShelfDummyPlugin(info={
|
||||
'name': 'shelve',
|
||||
'name': 'test_corrupt_shelf_{}'.format(msg),
|
||||
'description': 'Dummy plugin for tests',
|
||||
'version': 'test',
|
||||
'shelf_file': f.name
|
||||
})
|
||||
@@ -126,6 +167,7 @@ class PluginsTest(TestCase):
|
||||
''' Reusing the values of a previous shelf '''
|
||||
a = ShelfDummyPlugin(info={
|
||||
'name': 'shelve',
|
||||
'description': 'Dummy plugin for tests',
|
||||
'version': 'test',
|
||||
'shelf_file': self.shelf_file
|
||||
})
|
||||
@@ -136,6 +178,7 @@ class PluginsTest(TestCase):
|
||||
|
||||
b = ShelfDummyPlugin(info={
|
||||
'name': 'shelve',
|
||||
'description': 'Dummy plugin for tests',
|
||||
'version': 'test',
|
||||
'shelf_file': self.shelf_file
|
||||
})
|
||||
@@ -148,6 +191,7 @@ class PluginsTest(TestCase):
|
||||
''' Should be able to set extra parameters'''
|
||||
a = ShelfDummyPlugin(info={
|
||||
'name': 'shelve',
|
||||
'description': 'Dummy shelf plugin for tests',
|
||||
'version': 'test',
|
||||
'shelf_file': self.shelf_file,
|
||||
'extra_params': {
|
||||
@@ -160,6 +204,61 @@ class PluginsTest(TestCase):
|
||||
})
|
||||
assert 'example' in a.extra_params
|
||||
|
||||
def test_box(self):
|
||||
|
||||
class MyBox(plugins.Box):
|
||||
''' Vague description'''
|
||||
|
||||
author = 'me'
|
||||
version = 0
|
||||
|
||||
def input(self, entry, **kwargs):
|
||||
return entry.text
|
||||
|
||||
def predict_one(self, input):
|
||||
return 'SIGN' in input
|
||||
|
||||
def output(self, output, entry, **kwargs):
|
||||
if output:
|
||||
entry.myAnnotation = 'DETECTED'
|
||||
return entry
|
||||
|
||||
test_cases = [
|
||||
{
|
||||
'input': "nothing here",
|
||||
'expected': {'myAnnotation': 'DETECTED'},
|
||||
'should_fail': True
|
||||
}, {
|
||||
'input': "SIGN",
|
||||
'expected': {'myAnnotation': 'DETECTED'}
|
||||
}]
|
||||
|
||||
MyBox().test()
|
||||
|
||||
def test_sentimentbox(self):
|
||||
|
||||
class SentimentBox(plugins.MappingMixin, plugins.SentimentBox):
|
||||
''' Vague description'''
|
||||
|
||||
author = 'me'
|
||||
version = 0
|
||||
|
||||
mappings = {'happy': 'marl:Positive', 'sad': 'marl:Negative'}
|
||||
|
||||
def predict_one(self, input, **kwargs):
|
||||
return 'happy' if ':)' in input else 'sad'
|
||||
|
||||
test_cases = [
|
||||
{
|
||||
'input': 'a happy face :)',
|
||||
'polarity': 'marl:Positive'
|
||||
}, {
|
||||
'input': "Nothing",
|
||||
'polarity': 'marl:Negative'
|
||||
}]
|
||||
|
||||
SentimentBox().test()
|
||||
|
||||
def test_conversion_centroids(self):
|
||||
info = {
|
||||
"name": "CentroidTest",
|
||||
@@ -213,10 +312,46 @@ class PluginsTest(TestCase):
|
||||
res = c._backwards_conversion(e)
|
||||
assert res["onyx:hasEmotionCategory"] == "c2"
|
||||
|
||||
@skipIf(sys.version_info < (3, 0),
|
||||
reason="requires Python3")
|
||||
def test_evaluation(self):
|
||||
testdata = []
|
||||
for i in range(50):
|
||||
testdata.append(["good", 1])
|
||||
for i in range(50):
|
||||
testdata.append(["bad", 0])
|
||||
dataset = pd.DataFrame(testdata, columns=['text', 'polarity'])
|
||||
|
||||
def make_mini_test(plugin_info):
|
||||
class DummyPlugin(plugins.TextBox):
|
||||
description = 'Plugin to test evaluation'
|
||||
version = 0
|
||||
|
||||
def predict_one(self, input):
|
||||
return 0
|
||||
|
||||
class SmartPlugin(plugins.TextBox):
|
||||
description = 'Plugin to test evaluation'
|
||||
version = 0
|
||||
|
||||
def predict_one(self, input):
|
||||
if input == 'good':
|
||||
return 1
|
||||
return 0
|
||||
|
||||
dpipe = DummyPlugin()
|
||||
results = plugins.evaluate(datasets={'testdata': dataset}, plugins=[dpipe], flatten=True)
|
||||
dumb_metrics = results[0].metrics[0]
|
||||
assert abs(dumb_metrics['accuracy'] - 0.5) < 0.01
|
||||
|
||||
spipe = SmartPlugin()
|
||||
results = plugins.evaluate(datasets={'testdata': dataset}, plugins=[spipe], flatten=True)
|
||||
smart_metrics = results[0].metrics[0]
|
||||
assert abs(smart_metrics['accuracy'] - 1) < 0.01
|
||||
|
||||
|
||||
def make_mini_test(fpath):
|
||||
def mini_test(self):
|
||||
plugin = plugins.load_plugin_from_info(plugin_info, install=True)
|
||||
for plugin in plugins.from_path(fpath, install=True):
|
||||
plugin.test()
|
||||
return mini_test
|
||||
|
||||
@@ -224,11 +359,10 @@ def make_mini_test(plugin_info):
|
||||
def _add_tests():
|
||||
root = os.path.join(os.path.dirname(__file__), '..')
|
||||
print(root)
|
||||
plugs = plugins.load_plugins([root, ], loader=plugins.parse_plugin_info)
|
||||
for k, v in plugs.items():
|
||||
for fpath in plugins.find_plugins([root, ]):
|
||||
pass
|
||||
t_method = make_mini_test(v)
|
||||
t_method.__name__ = 'test_plugin_{}'.format(k)
|
||||
t_method = make_mini_test(fpath)
|
||||
t_method.__name__ = 'test_plugin_{}'.format(fpath)
|
||||
setattr(PluginsTest, t_method.__name__, t_method)
|
||||
del t_method
|
||||
|
||||
|
32
tests/test_test.py
Normal file
32
tests/test_test.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from unittest import TestCase
|
||||
|
||||
import requests
|
||||
import json
|
||||
from senpy.test import patch_requests
|
||||
from senpy.models import Results
|
||||
|
||||
|
||||
class TestTest(TestCase):
|
||||
def test_patch_text(self):
|
||||
with patch_requests('hello'):
|
||||
r = requests.get('http://example.com')
|
||||
assert r.text == 'hello'
|
||||
assert r.content == 'hello'
|
||||
|
||||
def test_patch_json(self):
|
||||
r = Results()
|
||||
with patch_requests(r):
|
||||
res = requests.get('http://example.com')
|
||||
assert res.content == json.dumps(r.jsonld())
|
||||
js = res.json()
|
||||
assert js
|
||||
assert js['@type'] == r['@type']
|
||||
|
||||
def test_patch_dict(self):
|
||||
r = {'nothing': 'new'}
|
||||
with patch_requests(r):
|
||||
res = requests.get('http://example.com')
|
||||
assert res.content == json.dumps(r)
|
||||
js = res.json()
|
||||
assert js
|
||||
assert js['nothing'] == 'new'
|
Reference in New Issue
Block a user