1
0
mirror of https://github.com/gsi-upm/senpy synced 2025-09-17 03:52:22 +00:00

Compare commits

..

9 Commits
0.2.6 ... 0.3.2

Author SHA1 Message Date
J. Fernando Sánchez
79c83e34a3 Added random plugin and other features 2015-02-23 02:13:31 +01:00
J. Fernando Sánchez
37a098109f Module script and improvement in JSON-LD 2014-12-02 13:31:15 +01:00
J. Fernando Sánchez
ff14925056 Improved plugins, better tests, gevent
Moved from Yapsy again (it is not flexible enough), now we use a
custom solution.
The activation and deactivation of plugins is asynchronous, so
that plugins that take a long time don't interfere with the rest.
2014-12-01 18:27:20 +01:00
J. Fernando Sánchez
10f4782ad7 Better NIF compliance 2014-12-01 09:38:23 +01:00
J. Fernando Sánchez
4351f76b60 Removed unnecessary contexts 2014-11-27 17:43:19 +01:00
J. Fernando Sánchez
86f45f8147 JSON-LD contexts and prefixes 2014-11-27 17:39:36 +01:00
J. Fernando Sánchez
2834967026 Better jsonld support 2014-11-27 11:27:05 +01:00
J. Fernando Sánchez
2f7a8d7267 Fixed setup.py and pip 2014-11-20 20:54:57 +01:00
J. Fernando Sánchez
2b68838514 PEP8 compliance 2014-11-20 19:29:49 +01:00
27 changed files with 807 additions and 331 deletions

4
MANIFEST.in Normal file
View File

@@ -0,0 +1,4 @@
include requirements.txt
include README.md
include senpy/context.jsonld
recursive-include *.senpy

26
app.py
View File

@@ -14,20 +14,30 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
''' """
Simple Sentiment Analysis server for EUROSENTIMENT This is a helper for development. If you want to run Senpy use:
This class shows how to use the nif_server module to create custom services. python -m senpy
''' """
from gevent.monkey import patch_all; patch_all()
import gevent
import config import config
from flask import Flask from flask import Flask
from senpy.extensions import Senpy from senpy.extensions import Senpy
import logging
import os
from gevent.wsgi import WSGIServer
logging.basicConfig(level=logging.DEBUG)
app = Flask(__name__) app = Flask(__name__)
mypath = os.path.dirname(os.path.realpath(__file__))
sp = Senpy() sp = Senpy(app, os.path.join(mypath, "plugins"))
sp.init_app(app) sp.activate_all()
if __name__ == '__main__': if __name__ == '__main__':
import logging
logging.basicConfig(level=config.DEBUG)
app.debug = config.DEBUG app.debug = config.DEBUG
app.run(host="0.0.0.0", use_reloader=False) http_server = WSGIServer(('', config.SERVER_PORT), app)
http_server.serve_forever()

1
dev-requirements.txt Normal file
View File

@@ -0,0 +1 @@
mock

View File

@@ -1,9 +0,0 @@
from senpy.plugins import SenpyPlugin
class Prueba(SenpyPlugin):
def __init__(self, **kwargs):
super(Prueba, self).__init__(name="prueba",
version="4.0",
**kwargs)
plugin = Prueba()

32
plugins/rand/rand.py Normal file
View File

@@ -0,0 +1,32 @@
import json
import random
from senpy.plugins import SentimentPlugin
from senpy.models import Response, Opinion, Entry
class Sentiment140Plugin(SentimentPlugin):
def analyse(self, **params):
lang = params.get("language", "auto")
p = params.get("prefix", None)
response = Response(prefix=p)
#polarity_value = self.maxPolarityValue*int(res.json()["data"][0]["polarity"]) * 0.25
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"
entry = Entry(id="Entry0",
text=params["input"],
prefix=p)
opinion = Opinion(id="Opinion0",
prefix=p,
hasPolarity=polarity,
polarityValue=polarity_value)
opinion["prov:wasGeneratedBy"] = self.id
entry.opinions.append(opinion)
entry.language = lang
response.entries.append(entry)
return response

17
plugins/rand/rand.senpy Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "rand",
"module": "rand",
"description": "What my plugin broadly does",
"author": "@balkian",
"version": "0.1",
"extra_params": {
"language": {
"aliases": ["language", "l"],
"required": false,
"options": ["es", "en", "auto"]
}
},
"requirements": {},
"marl:maxPolarityValue": "1",
"marl:minPolarityValue": "-1"
}

View File

@@ -1,47 +0,0 @@
import requests
import json
import sys
from senpy.plugins import SentimentPlugin
from senpy.models import Response, Opinion, Entry
class Sentiment140Plugin(SentimentPlugin):
EXTRA_PARAMS = {
"language": {"aliases": ["language", "l"],
"required": False,
"options": ["es", "en", "auto"],
}
}
def __init__(self, **kwargs):
super(Sentiment140Plugin, self).__init__(name="sentiment140",
version="2.0",
extraparams=self.EXTRA_PARAMS,
**kwargs)
def analyse(self, **params):
lang = params.get("language", "auto")
res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson",
json.dumps({
"language": lang,
"data": [{"text": params["input"]}]}
))
response = Response()
polarityValue = int(res.json()["data"][0]["polarity"]) * 25
polarity = "marl:Neutral"
if polarityValue > 50:
polarity = "marl:Positive"
elif polarityValue < 50:
polarity = "marl:Negative"
entry = Entry(text=params["input"])
opinion = Opinion(polarity=polarity, polarityValue=polarityValue)
entry.opinions.append(opinion)
entry.language = lang
response.entries.append(entry)
return response
plugin = Sentiment140Plugin()

View File

@@ -0,0 +1,37 @@
import requests
import json
from senpy.plugins import SentimentPlugin
from senpy.models import Response, Opinion, Entry
class Sentiment140Plugin(SentimentPlugin):
def analyse(self, **params):
lang = params.get("language", "auto")
res = requests.post("http://www.sentiment140.com/api/bulkClassifyJson",
json.dumps({"language": lang,
"data": [{"text": params["input"]}]
}
)
)
p = params.get("prefix", None)
response = Response(prefix=p)
polarity_value = self.maxPolarityValue*int(res.json()["data"][0]["polarity"]) * 0.25
polarity = "marl:Neutral"
if polarity_value > 50:
polarity = "marl:Positive"
elif polarity_value < 50:
polarity = "marl:Negative"
entry = Entry(id="Entry0",
text=params["input"],
prefix=p)
opinion = Opinion(id="Opinion0",
prefix=p,
hasPolarity=polarity,
polarityValue=polarity_value)
opinion["prov:wasGeneratedBy"] = self.id
entry.opinions.append(opinion)
entry.language = lang
response.entries.append(entry)
return response

View File

@@ -0,0 +1,17 @@
{
"name": "sentiment140",
"module": "sentiment140",
"description": "What my plugin broadly does",
"author": "@balkian",
"version": "0.1",
"extra_params": {
"language": {
"aliases": ["language", "l"],
"required": false,
"options": ["es", "en", "auto"]
}
},
"requirements": {},
"maxPolarityValue": "1",
"minPolarityValue": "0"
}

View File

@@ -1,5 +1,7 @@
Flask==0.10.1 Flask==0.10.1
gunicorn==19.0.0 gunicorn==19.0.0
requests==2.4.1 requests==2.4.1
Flask-Plugins==1.4
GitPython==0.3.2.RC1 GitPython==0.3.2.RC1
Yapsy>=1.10.423
gevent>=1.0.1
PyLD>=0.6.5

View File

@@ -14,21 +14,10 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
''' """
Sentiment analysis server in Python Sentiment analysis server in Python
''' """
VERSION = "0.2.6"
import extensions import extensions
import blueprints import blueprints
import plugins import plugins
if __name__ == '__main__':
from flask import Flask
app = Flask(__name__)
sp = extensions.Senpy()
sp.init_app(app)
app.debug = config.DEBUG
app.run()

View File

@@ -1,7 +1,72 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright 2014 J. Fernando Sánchez Rada - Grupo de Sistemas Inteligentes
# DIT, UPM
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Senpy is a modular sentiment analysis server. This script runs an instance of
the server.
"""
from gevent.monkey import patch_all; patch_all(thread=False)
import gevent
from flask import Flask from flask import Flask
from extensions import Senpy from senpy.extensions import Senpy
import logging
import os
from gevent.wsgi import WSGIServer
import argparse
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Run a Senpy server')
parser.add_argument('--level',
"-l",
metavar="logging_level",
type=str,
default="INFO",
help='Logging level')
parser.add_argument('--debug',
"-d",
action='store_true',
default=False,
help='Run the application in debug mode')
parser.add_argument('--host',
type=str,
default = "127.0.0.1",
help='Use 0.0.0.0 to accept requests from any host.')
parser.add_argument('--port',
'-p',
type=int,
default = 5000,
help='Port to listen on.')
parser.add_argument('--plugins-folder',
'-f',
type=str,
default = "plugins",
help='Where to look for plugins.')
args = parser.parse_args()
logging.basicConfig(level=getattr(logging,args.level))
app = Flask(__name__) app = Flask(__name__)
sp = Senpy() app.debug = args.debug
sp.init_app(app) sp = Senpy(app, args.plugins_folder)
app.debug = True sp.activate_all()
app.run() import logging
http_server = WSGIServer((args.host, args.port), app)
try:
print "Server running on port %s:%d. Ctrl+C to quit" % (args.host,
args.port)
http_server.serve_forever()
except KeyboardInterrupt:
http_server.stop()
print "Bye!"

View File

@@ -14,14 +14,15 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
''' """
Simple Sentiment Analysis server Blueprints for Senpy
''' """
import json import json
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from flask import Blueprint, render_template, request, jsonify, current_app from flask import Blueprint, request, jsonify, current_app
nif_blueprint = Blueprint("NIF Sentiment Analysis Server", __name__) nif_blueprint = Blueprint("NIF Sentiment Analysis Server", __name__)
@@ -31,8 +32,8 @@ BASIC_PARAMS = {
}, },
} }
def get_params(req, params=BASIC_PARAMS): def get_params(req, params=BASIC_PARAMS):
indict = None
if req.method == 'POST': if req.method == 'POST':
indict = req.form indict = req.form
elif req.method == 'GET': elif req.method == 'GET':
@@ -41,37 +42,37 @@ def get_params(req, params=BASIC_PARAMS):
raise ValueError("Invalid data") raise ValueError("Invalid data")
outdict = {} outdict = {}
wrongParams = {} wrong_params = {}
for param, options in params.iteritems(): for param, options in params.iteritems():
for alias in options["aliases"]: for alias in options["aliases"]:
if alias in indict: if alias in indict:
outdict[param] = indict[alias] outdict[param] = indict[alias]
if param not in outdict: if param not in outdict:
if options.get("required", False): if options.get("required", False) and "default" not in options:
wrongParams[param] = params[param] wrong_params[param] = params[param]
else: else:
if "default" in options: if "default" in options:
outdict[param] = options["default"] outdict[param] = options["default"]
else: else:
if "options" in params[param] and \ if "options" in params[param] and outdict[param] not in params[param]["options"]:
outdict[param] not in params[param]["options"]: wrong_params[param] = params[param]
wrongParams[param] = params[param] if wrong_params:
if wrongParams: message = {"status": "failed",
message = {"status": "failed", "message": "Missing or invalid parameters"} "message": "Missing or invalid parameters",
message["parameters"] = outdict "parameters": outdict,
message["errors"] = {param:error for param, error in wrongParams.iteritems()} "errors": {param: error for param, error in wrong_params.iteritems()}
raise ValueError(json.dumps(message)) }
raise ValueError(message)
return outdict return outdict
def basic_analysis(params): def basic_analysis(params):
response = {"@context": ["http://demos.gsi.dit.upm.es/eurosentiment/static/context.jsonld", response = {"@context": ["http://demos.gsi.dit.upm.es/eurosentiment/static/context.jsonld",
{ {
"@base": "{}#".format(request.url.encode('utf-8')) "@base": "{}#".format(request.url.encode('utf-8'))
} }
], ],
"analysis": [{ "analysis": [{"@type": "marl:SentimentAnalysis"}],
"@type": "marl:SentimentAnalysis"
}],
"entries": [] "entries": []
} }
if "language" in params: if "language" in params:
@@ -83,53 +84,58 @@ def basic_analysis(params):
}) })
return response return response
@nif_blueprint.route('/', methods=['POST', 'GET']) @nif_blueprint.route('/', methods=['POST', 'GET'])
def home(entries=None): def home():
try: try:
algo = get_params(request).get("algorithm", None) params = get_params(request)
algo = params.get("algorithm", None)
specific_params = current_app.senpy.parameters(algo) specific_params = current_app.senpy.parameters(algo)
params = get_params(request, specific_params) params.update(get_params(request, specific_params))
response = current_app.senpy.analyse(**params) response = current_app.senpy.analyse(**params)
return jsonify(response) return jsonify(response)
except ValueError as ex: except ValueError as ex:
return ex.message return jsonify(ex.message)
except Exception as ex: except Exception as ex:
return jsonify(status="400", message=ex.message) return jsonify(status="400", message=ex.message)
@nif_blueprint.route("/default") @nif_blueprint.route("/default")
def default(): def default():
return current_app.senpy.default_plugin return current_app.senpy.default_plugin
#return plugins(action="list", plugin=current_app.senpy.default_algorithm) #return plugins(action="list", plugin=current_app.senpy.default_algorithm)
@nif_blueprint.route('/plugins/', methods=['POST', 'GET']) @nif_blueprint.route('/plugins/', methods=['POST', 'GET'])
@nif_blueprint.route('/plugins/<plugin>', methods=['POST', 'GET']) @nif_blueprint.route('/plugins/<plugin>', methods=['POST', 'GET'])
@nif_blueprint.route('/plugins/<plugin>/<action>', methods=['POST', 'GET']) @nif_blueprint.route('/plugins/<plugin>/<action>', methods=['POST', 'GET'])
def plugins(plugin=None, action="list"): def plugins(plugin=None, action="list"):
filt = {} filt = {}
sp = current_app.senpy
if plugin: if plugin:
filt["name"] = plugin filt["name"] = plugin
plugs = current_app.senpy.filter_plugins(**filt) plugs = sp.filter_plugins(**filt)
if plugin and not plugs: if plugin and not plugs:
return "Plugin not found", 400 return "Plugin not found", 400
if action == "list": if action == "list":
with_params = request.args.get("params", "") == "1" with_params = request.args.get("params", "") == "1"
if plugin:
dic = plugs[plugin].jsonable(with_params)
else:
dic = {plug: plugs[plug].jsonable(with_params) for plug in plugs} dic = {plug: plugs[plug].jsonable(with_params) for plug in plugs}
return jsonify(dic) return jsonify(dic)
if action == "disable": method = "{}_plugin".format(action)
current_app.senpy.disable_plugin(plugin) if(hasattr(sp, method)):
return "Ok" getattr(sp, method)(plugin)
elif action == "enable":
current_app.senpy.enable_plugin(plugin)
return "Ok"
elif action == "reload":
current_app.senpy.reload_plugin(plugin)
return "Ok" return "Ok"
else: else:
return "action '{}' not allowed".format(action), 400 return "action '{}' not allowed".format(action), 400
if __name__ == '__main__': if __name__ == '__main__':
import config import config
from flask import Flask from flask import Flask
app = Flask(__name__) app = Flask(__name__)
app.register_blueprint(nif_blueprint) app.register_blueprint(nif_blueprint)
app.debug = config.DEBUG app.debug = config.DEBUG

View File

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

View File

@@ -1,19 +1,22 @@
"""
"""
import os import os
import fnmatch
import inspect
import sys import sys
import imp import imp
import logging import logging
import gevent
import json
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from .plugins import SentimentPlugin, EmotionPlugin from .plugins import SenpyPlugin, SentimentPlugin, EmotionPlugin
from .models import Error
try:
from flask import _app_ctx_stack as stack
except ImportError:
from flask import _request_ctx_stack as stack
from .blueprints import nif_blueprint from .blueprints import nif_blueprint
from git import Repo, InvalidGitRepositoryError from git import Repo, InvalidGitRepositoryError
from functools import partial
class Senpy(object): class Senpy(object):
@@ -48,14 +51,15 @@ class Senpy(object):
app.register_blueprint(nif_blueprint) app.register_blueprint(nif_blueprint)
def add_folder(self, folder): def add_folder(self, folder):
logger.debug("Adding folder: %s", folder)
if os.path.isdir(folder): if os.path.isdir(folder):
self._search_folders.add(folder) self._search_folders.add(folder)
self._outdated = True self._outdated = True
return True return True
else: else:
logger.debug("Not a folder: %s", folder)
return False return False
def analyse(self, **params): def analyse(self, **params):
algo = None algo = None
logger.debug("analysing with params: {}".format(params)) logger.debug("analysing with params: {}".format(params))
@@ -63,18 +67,23 @@ class Senpy(object):
algo = params["algorithm"] algo = params["algorithm"]
elif self.plugins: elif self.plugins:
algo = self.default_plugin algo = self.default_plugin
if algo in self.plugins and self.plugins[algo].enabled: if algo in self.plugins:
if self.plugins[algo].is_activated:
plug = self.plugins[algo] plug = self.plugins[algo]
resp = plug.analyse(**params) resp = plug.analyse(**params)
resp.analysis.append(plug.jsonable()) resp.analysis.append(plug)
return resp return resp
else: else:
return {"status": 400, "message": "The algorithm '{}' is not valid".format(algo) } logger.debug("Plugin not activated: {}".format(algo))
return Error(status=400, message="The algorithm '{}' is not activated yet".format(algo))
else:
logger.debug("The algorithm '{}' is not valid\nValid algorithms: {}".format(algo, self.plugins.keys()))
return Error(status=400, message="The algorithm '{}' is not valid".format(algo))
@property @property
def default_plugin(self): def default_plugin(self):
candidates = self.filter_plugins(enabled=True) candidates = self.filter_plugins(is_activated=True)
if len(candidates)>1: if len(candidates) > 0:
candidate = candidates.keys()[0] candidate = candidates.keys()[0]
logger.debug("Default: {}".format(candidate)) logger.debug("Default: {}".format(candidate))
return candidate return candidate
@@ -84,11 +93,38 @@ class Senpy(object):
def parameters(self, algo): def parameters(self, algo):
return getattr(self.plugins.get(algo or self.default_plugin), "params", {}) return getattr(self.plugins.get(algo or self.default_plugin), "params", {})
def enable_plugin(self, plugin): def activate_all(self, sync=False):
self.plugins[plugin].enable() ps = []
for plug in self.plugins.keys():
ps.append(self.activate_plugin(plug, sync=sync))
return ps
def disable_plugin(self, plugin): def deactivate_all(self, sync=False):
self.plugins[plugin].disable() ps = []
for plug in self.plugins.keys():
ps.append(self.deactivate_plugin(plug, sync=sync))
return ps
def _set_active_plugin(self, plugin_name, active=True, *args, **kwargs):
self.plugins[plugin_name].is_activated = active
def activate_plugin(self, plugin_name, sync=False):
plugin = self.plugins[plugin_name]
th = gevent.spawn(plugin.activate)
th.link_value(partial(self._set_active_plugin, plugin_name, True))
if sync:
th.join()
else:
return th
def deactivate_plugin(self, plugin_name, sync=False):
plugin = self.plugins[plugin_name]
th = gevent.spawn(plugin.deactivate)
th.link_value(partial(self._set_active_plugin, plugin_name, False))
if sync:
th.join()
else:
return th
def reload_plugin(self, plugin): def reload_plugin(self, plugin):
logger.debug("Reloading {}".format(plugin)) logger.debug("Reloading {}".format(plugin))
@@ -97,38 +133,48 @@ class Senpy(object):
del self.plugins[plugin] del self.plugins[plugin]
self.plugins[nplug.name] = nplug self.plugins[nplug.name] = nplug
def _load_plugin(self, plugin, search_folder, enabled=True): @staticmethod
logger.debug("Loading plugins") def _load_plugin(root, filename):
sys.path.append(search_folder) logger.debug("Loading plugin: {}".format(filename))
(fp, pathname, desc) = imp.find_module(plugin) fpath = os.path.join(root, filename)
with open(fpath,'r') as f:
info = json.load(f)
logger.debug("Info: {}".format(info))
sys.path.append(root)
module = info["module"]
name = info["name"]
(fp, pathname, desc) = imp.find_module(module, [root,])
try: try:
tmp = imp.load_module(plugin, fp, pathname, desc).plugin tmp = imp.load_module(module, fp, pathname, desc)
sys.path.remove(search_folder) sys.path.remove(root)
tmp.path = search_folder 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(filename))
return
module = candidate(info=info)
try: try:
repo_path = os.path.join(search_folder, plugin) repo_path = root
tmp.repo = Repo(repo_path) module._repo = Repo(repo_path)
except InvalidGitRepositoryError: except InvalidGitRepositoryError:
tmp.repo = None module._repo = None
if not hasattr(tmp, "enabled"):
tmp.enabled = enabled
tmp.module = plugin
except Exception as ex: except Exception as ex:
tmp = None logger.debug("Exception importing {}: {}".format(filename, ex))
logger.debug("Exception importing {}: {}".format(plugin, ex)) return None, None
return tmp return name, module
def _load_plugins(self): def _load_plugins(self):
plugins = {} plugins = {}
for search_folder in self._search_folders: for search_folder in self._search_folders:
for item in os.listdir(search_folder): for root, dirnames, filenames in os.walk(search_folder):
if os.path.isdir(os.path.join(search_folder, item)) \ for filename in fnmatch.filter(filenames, '*.senpy'):
and os.path.exists( name, plugin = self._load_plugin(root, filename)
os.path.join(search_folder, item, "__init__.py")):
plugin = self._load_plugin(item, search_folder)
if plugin: if plugin:
plugins[plugin.name] = plugin plugins[name] = plugin
self._outdated = False self._outdated = False
return plugins return plugins
@@ -136,33 +182,23 @@ class Senpy(object):
def teardown(self, exception): def teardown(self, exception):
pass pass
def enable_all(self):
for plugin in self.plugins:
self.enable_plugin(plugin)
def enable_plugin(self, item):
self.plugins[item].enabled = True
def disable_plugin(self, item):
self.plugins[item].enabled = False
@property @property
def plugins(self): def plugins(self):
""" Return the plugins registered for a given application. """ """ Return the plugins registered for a given application. """
ctx = stack.top if not hasattr(self, 'senpy_plugins') or self._outdated:
if ctx is not None: self.senpy_plugins = self._load_plugins()
if not hasattr(ctx, 'senpy_plugins') or self._outdated: return self.senpy_plugins
ctx.senpy_plugins = self._load_plugins()
return ctx.senpy_plugins
def filter_plugins(self, **kwargs): def filter_plugins(self, **kwargs):
""" Filter plugins by different criteria """ """ Filter plugins by different criteria """
def matches(plug): def matches(plug):
res = all(getattr(plug, k, None) == v for (k, v) in kwargs.items()) res = all(getattr(plug, k, None) == v for (k, v) in kwargs.items())
logger.debug("matching {} with {}: {}".format(plug.name, logger.debug("matching {} with {}: {}".format(plug.name,
kwargs, kwargs,
res)) res))
return res return res
if not kwargs: if not kwargs:
return self.plugins return self.plugins
else: else:
@@ -172,12 +208,3 @@ class Senpy(object):
""" Return only the sentiment plugins """ """ Return only the sentiment plugins """
return {p: plugin for p, plugin in self.plugins.items() if return {p: plugin for p, plugin in self.plugins.items() if
isinstance(plugin, SentimentPlugin)} isinstance(plugin, SentimentPlugin)}
if __name__ == '__main__':
from flask import Flask
app = Flask(__name__)
sp = Senpy()
sp.init_app(APP)
with APP.app_context():
sp._load_plugins()

View File

@@ -1,59 +1,223 @@
import json import json
import os import os
from collections import defaultdict from collections import defaultdict
from pyld import jsonld
class Leaf(defaultdict):
def __init__(self, ofclass=list):
super(Leaf, self).__init__(ofclass)
def __getattr__(self, name):
return super(Leaf, self).__getitem__(name)
def __setattr__(self, name, value): class Leaf(dict):
self[name] = value _prefix = None
_frame = {}
_context = {}
def __init__(self,
id=None,
context=None,
vocab=None,
prefix=None,
frame=None):
super(Leaf, self).__init__()
if context is not None:
self.context = context
elif self._context:
self.context = self._context
else:
self.context = {}
if frame is not None:
self._frame = frame
self._prefix = prefix
self.id = id
def __getattr__(self, key):
try:
return object.__getattr__(self, key)
except AttributeError:
try:
return super(Leaf, self).__getitem__(self._get_key(key))
except KeyError:
raise AttributeError()
def __setattr__(self, key, value):
try:
object.__getattr__(self, key)
object.__setattr__(self, key, value)
except AttributeError:
key = self._get_key(key)
if key == "@context":
value = self.get_context(value)
elif key == "@id":
value = self.get_id(value)
if key[0] == "_":
object.__setattr__(self, key, value)
else:
if value is None:
try:
super(Leaf, self).__delitem__(key)
except KeyError:
pass
else:
super(Leaf, self).__setitem__(key, value)
def get_id(self, id):
"""
This is not the most elegant solution to change the @id attribute, but it
is the quickest way to have it included in the dictionary without extra
boilerplate.
"""
if id and self._prefix and ":" not in id:
return "{}{}".format(self._prefix, id)
else:
return id
def __delattr__(self, key):
return super(Leaf, self).__delitem__(self._get_key(key))
def _get_key(self, key):
if key[0] == "_":
return key
elif key in ["context", "id"]:
return "@{}".format(key)
else:
return key
@staticmethod
def get_context(context):
if isinstance(context, list):
contexts = []
for c in context:
contexts.append(Response.get_context(c))
return contexts
elif isinstance(context, dict):
return context
elif isinstance(context, basestring):
try:
with open(context) as f:
return json.loads(f.read())
except IOError:
return context
def compact(self):
return jsonld.compact(self, self.context)
def frame(self, frame=None, options=None):
if frame is None:
frame = self._frame
if options is None:
options = {}
return jsonld.frame(self, frame, options)
def jsonable(self, parameters=False, frame=None, options=None, context=None):
if frame is None:
frame = self._frame
if options is None:
options = {}
if context is None:
context = self._context
return jsonld.compact(jsonld.frame(self, frame, options), context)
#if parameters:
#resp["parameters"] = self.params
#elif self.extra_params:
#resp["extra_parameters"] = self.extra_params
#return resp
def to_JSON(self):
return json.dumps(self,
default=lambda o: o.__dict__,
sort_keys=True, indent=4)
def __delattr__(self, name):
return super(Leaf, self).__delitem__(name)
class Response(Leaf): class Response(Leaf):
def __init__(self, context=None): _frame = { "@context": {
super(Response, self).__init__() "analysis": {
self["analysis"] = [] "@container": "@set",
self["entries"] = [] "@id": "prov:wasInformedBy"
},
"date": {
"@id": "dc:date",
"@type": "xsd:dateTime"
},
"dc": "http://purl.org/dc/terms/",
"dc:subject": {
"@type": "@id"
},
"emotions": {
"@container": "@set",
"@id": "onyx:hasEmotionSet"
},
"entries": {
"@container": "@set",
"@id": "prov:generated"
},
"marl": "http://www.gsi.dit.upm.es/ontologies/marl/ns#",
"nif": "http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#",
"onyx": "http://www.gsi.dit.upm.es/ontologies/onyx/ns#",
"opinions": {
"@container": "@set",
"@id": "marl:hasOpinion"
},
"prov": "http://www.w3.org/ns/prov#",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"strings": {
"@container": "@set",
"@reverse": "nif:hasContext"
},
"wnaffect": "http://www.gsi.dit.upm.es/ontologies/wnaffect#",
"xsd": "http://www.w3.org/2001/XMLSchema#"
},
"analysis": {},
"entries": {}
}
def __init__(self, context=None, *args, **kwargs):
if context is None: if context is None:
context = "{}/context.jsonld".format(os.path.dirname( context = "{}/context.jsonld".format(os.path.dirname(
os.path.realpath(__file__))) os.path.realpath(__file__)))
if isinstance(context, dict): super(Response, self).__init__(*args, context=context, **kwargs)
self["@context"] = context self.analysis = []
if isinstance(context, basestring): self.entries = []
try:
with open(context) as f:
self["@context"] = json.loads(f.read())
except IOError:
self["@context"] = context
class Entry(Leaf): class Entry(Leaf):
def __init__(self, text=None, emotionSets=None, opinions=None, **kwargs): _context = {
"@vocab": "http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#"
}
def __init__(self, text=None, emotion_sets=None, opinions=None, **kwargs):
super(Entry, self).__init__(**kwargs) super(Entry, self).__init__(**kwargs)
if text: if text:
self.text = text self.text = text
if emotionSets: self.emotionSets = emotion_sets if emotion_sets else []
self.emotionSets = emotionSets self.opinions = opinions if opinions else []
if opinions:
self.opinions = opinions
class Opinion(Leaf): class Opinion(Leaf):
def __init__(self, polarityValue=None, polarity=None, **kwargs): _context = {
super(Opinion, self).__init__(**kwargs) "@vocab": "http://www.gsi.dit.upm.es/ontologies/marl/ns#"
}
def __init__(self, polarityValue=None, hasPolarity=None, *args, **kwargs):
super(Opinion, self).__init__(*args,
**kwargs)
if polarityValue is not None: if polarityValue is not None:
self.polarityValue = polarityValue self.hasPolarityValue = polarityValue
if polarity is not None: if hasPolarity is not None:
self.polarity = polarity self.hasPolarity = hasPolarity
class EmotionSet(Leaf): class EmotionSet(Leaf):
def __init__(self, emotions=[], **kwargs): _context = {}
super(EmotionSet, self).__init__(**kwargs) def __init__(self, emotions=None, *args, **kwargs):
if not emotions:
emotions = []
super(EmotionSet, self).__init__(context=EmotionSet._context,
*args,
**kwargs)
self.emotions = emotions or [] self.emotions = emotions or []
class Emotion(Leaf):
_context = {}
class Error(Leaf):
def __init__(self, *args, **kwargs):
super(Error, self).__init__(*args)
self.update(kwargs)

View File

@@ -1,4 +1,6 @@
import logging import logging
import ConfigParser
from .models import Leaf
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -25,6 +27,10 @@ PARAMS = {"input": {"aliases": ["i", "input"],
"required": False, "required": False,
"options": ["es", "en"], "options": ["es", "en"],
}, },
"prefix": {"aliases": ["prefix", "p"],
"required": True,
"default": "",
},
"urischeme": {"aliases": ["urischeme", "u"], "urischeme": {"aliases": ["urischeme", "u"],
"required": False, "required": False,
"default": "RFC5147String", "default": "RFC5147String",
@@ -32,70 +38,55 @@ PARAMS = {"input": {"aliases": ["i", "input"],
}, },
} }
class SenpyPlugin(object):
def __init__(self, name=None, version=None, extraparams=None, params=None): class SenpyPlugin(Leaf):
logger.debug("Initialising {}".format(name)) _context = {"@vocab": "http://www.gsi.dit.upm.es/ontologies/senpy/ns#",
self.name = name "info": None}
self.version = version _frame = { "@context": _context,
if params: "name": {},
self.params = params "@explicit": False,
else: "version": {},
self.params = PARAMS.copy() "repo": None,
if extraparams: "info": None,
self.params.update(extraparams) }
self.extraparams = extraparams or {} def __init__(self, info=None):
self.enabled = True if not info:
raise ValueError("You need to provide configuration information for the plugin.")
logger.debug("Initialising {}".format(info))
super(SenpyPlugin, self).__init__()
self.name = info["name"]
self.version = info["version"]
self.id="{}_{}".format(self.name, self.version)
self.params = info.get("params", PARAMS.copy())
self.extra_params = info.get("extra_params", {})
self.params.update(self.extra_params)
self.is_activated = False
self.info = info
def analyse(self, *args, **kwargs): def analyse(self, *args, **kwargs):
logger.debug("Analysing with: {} {}".format(self.name, self.version))
pass pass
def enable(self): def activate(self):
self.enabled = True pass
def disable(self): def deactivate(self):
self.enabled = False pass
@property
def id(self):
return "{}_{}".format(self.name, self.version)
def jsonable(self, parameters=False):
resp = {
"@id": "{}_{}".format(self.name, self.version),
"enabled": self.enabled,
}
if self.repo:
resp["repo"] = self.repo.remotes[0].url
if parameters:
resp["parameters"] = self.params
elif self.extraparams:
resp["extra_parameters"] = self.extraparams
return resp
class SentimentPlugin(SenpyPlugin): class SentimentPlugin(SenpyPlugin):
def __init__(self, def __init__(self, info, *args, **kwargs):
minPolarityValue=0, super(SentimentPlugin, self).__init__(info, *args, **kwargs)
maxPolarityValue=1, self.minPolarityValue = float(info.get("minPolarityValue", 0))
**kwargs): self.maxPolarityValue = float(info.get("maxPolarityValue", 1))
super(SentimentPlugin, self).__init__(**kwargs)
self.minPolarityValue = minPolarityValue
self.maxPolarityValue = maxPolarityValue
def jsonable(self, *args, **kwargs):
resp = super(SentimentPlugin, self).jsonable(*args, **kwargs)
resp["marl:maxPolarityValue"] = self.maxPolarityValue
resp["marl:minPolarityValue"] = self.minPolarityValue
return resp
class EmotionPlugin(SenpyPlugin): class EmotionPlugin(SenpyPlugin):
def __init__(self, def __init__(self, info, *args, **kwargs):
minEmotionValue=0, resp = super(EmotionPlugin, self).__init__(info, *args, **kwargs)
maxEmotionValue=1, self.minEmotionValue = float(info.get("minEmotionValue", 0))
emotionCategory=None, self.maxEmotionValue = float(info.get("maxEmotionValue", 0))
**kwargs):
super(EmotionPlugin, self).__init__(**kwargs)
self.minEmotionValue = minEmotionValue
self.maxEmotionValue = maxEmotionValue
self.emotionCategory = emotionCategory
def jsonable(self, *args, **kwargs):
resp = super(EmotionPlugin, self).jsonable(*args, **kwargs)
resp["onyx:minEmotionValue"] = self.minEmotionValue
resp["onyx:maxEmotionValue"] = self.maxEmotionValue
return resp

View File

@@ -1,10 +1,21 @@
from setuptools import setup from setuptools import setup
import senpy from pip.req import parse_requirements
# parse_requirements() returns generator of pip.req.InstallRequirement objects
install_reqs = parse_requirements("requirements.txt")
# reqs is a list of requirement
# e.g. ['django==1.5.1', 'mezzanine==1.4.6']
reqs = [str(ir.req) for ir in install_reqs]
VERSION = "0.3.2"
print(reqs)
setup( setup(
name='senpy', name='senpy',
packages=['senpy'], # this must be the same as the name above packages=['senpy'], # this must be the same as the name above
version = senpy.VERSION, version=VERSION,
description=''' description='''
A sentiment analysis server implementation. Designed to be \ A sentiment analysis server implementation. Designed to be \
extendable, so new algorithms and sources can be used. extendable, so new algorithms and sources can be used.
@@ -12,7 +23,9 @@ extendable, so new algorithms and sources can be used.
author='J. Fernando Sanchez', author='J. Fernando Sanchez',
author_email='balkian@gmail.com', author_email='balkian@gmail.com',
url='https://github.com/balkian/senpy', # use the URL to the github repo url='https://github.com/balkian/senpy', # use the URL to the github repo
download_url = 'https://github.com/balkian/senpy/archive/{}.tar.gz'.format(senpy.VERSION), download_url='https://github.com/balkian/senpy/archive/{}.tar.gz'.format(VERSION),
keywords=['eurosentiment', 'sentiment', 'emotions', 'nif'], # arbitrary keywords keywords=['eurosentiment', 'sentiment', 'emotions', 'nif'], # arbitrary keywords
classifiers=[], classifiers=[],
install_requires=reqs,
include_package_data = True,
) )

View File

@@ -1,5 +1,6 @@
import os import os
import logging import logging
try: try:
import unittest.mock as mock import unittest.mock as mock
except ImportError: except ImportError:
@@ -7,20 +8,23 @@ except ImportError:
from senpy.extensions import Senpy from senpy.extensions import Senpy
from flask import Flask from flask import Flask
from flask.ext.testing import TestCase from flask.ext.testing import TestCase
from gevent import sleep
def check_dict(indic, template): def check_dict(indic, template):
return all(item in indic.items() for item in template.items()) return all(item in indic.items() for item in template.items())
class Blueprints_Test(TestCase):
class BlueprintsTest(TestCase):
def create_app(self): def create_app(self):
self.app = Flask("test_extensions") self.app = Flask("test_extensions")
self.senpy = Senpy() self.senpy = Senpy()
self.senpy.init_app(self.app) self.senpy.init_app(self.app)
self.dir = os.path.join(os.path.dirname(__file__), "..") self.dir = os.path.join(os.path.dirname(__file__), "..")
self.senpy.add_folder(self.dir) self.senpy.add_folder(self.dir)
self.senpy.activate_plugin("Dummy", sync=True)
return self.app return self.app
def test_home(self): def test_home(self):
""" Calling with no arguments should ask the user for more arguments """ """ Calling with no arguments should ask the user for more arguments """
resp = self.client.get("/") resp = self.client.get("/")
@@ -33,3 +37,43 @@ class Blueprints_Test(TestCase):
} }
assert check_dict(resp.json, atleast) assert check_dict(resp.json, atleast)
def test_analysis(self):
""" The dummy plugin returns an empty response, it should contain the context """
resp = self.client.get("/?i=My aloha mohame")
self.assert200(resp)
logging.debug(resp.json)
assert "@context" in resp.json
assert check_dict(resp.json["@context"], {"marl": "http://www.gsi.dit.upm.es/ontologies/marl/ns#"})
assert "entries" in resp.json
def test_list(self):
""" List the plugins """
resp = self.client.get("/plugins/")
self.assert200(resp)
logging.debug(resp.json)
assert "Dummy" in resp.json
def test_detail(self):
""" Show only one plugin"""
resp = self.client.get("/plugins/Dummy")
self.assert200(resp)
logging.debug(resp.json)
assert "@id" in resp.json
assert resp.json["@id"] == "Dummy_0.1"
def test_activate(self):
""" Activate and deactivate one plugin """
resp = self.client.get("/plugins/Dummy/deactivate")
self.assert200(resp)
sleep(0.5)
resp = self.client.get("/plugins/Dummy")
self.assert200(resp)
assert "is_activated" in resp.json
assert resp.json["is_activated"] == False
resp = self.client.get("/plugins/Dummy/activate")
self.assert200(resp)
sleep(0.5)
resp = self.client.get("/plugins/Dummy")
self.assert200(resp)
assert "is_activated" in resp.json
assert resp.json["is_activated"] == True

40
tests/context.jsonld Normal file
View File

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

View File

@@ -1,3 +0,0 @@
from senpy.plugins import SenpyPlugin
plugin = SenpyPlugin("dummy")

View File

@@ -0,0 +1,6 @@
from senpy.plugins import SentimentPlugin
from senpy.models import Response
class DummyPlugin(SentimentPlugin):
def analyse(self, *args, **kwargs):
return Response()

View File

@@ -0,0 +1,7 @@
{
"name": "Dummy",
"module": "dummy",
"description": "I am dummy",
"author": "@balkian",
"version": "0.1"
}

View File

@@ -1,5 +1,6 @@
import os import os
import logging import logging
try: try:
import unittest.mock as mock import unittest.mock as mock
except ImportError: except ImportError:
@@ -9,62 +10,64 @@ from flask import Flask
from flask.ext.testing import TestCase from flask.ext.testing import TestCase
class Extensions_Test(TestCase): class ExtensionsTest(TestCase):
def create_app(self): def create_app(self):
self.app = Flask("test_extensions") self.app = Flask("test_extensions")
self.senpy = Senpy()
self.senpy.init_app(self.app)
self.dir = os.path.join(os.path.dirname(__file__), "..") self.dir = os.path.join(os.path.dirname(__file__), "..")
self.senpy.add_folder(self.dir) self.senpy = Senpy(plugin_folder=self.dir)
self.senpy.init_app(self.app)
self.senpy.activate_plugin("Dummy", sync=True)
return self.app return self.app
def test_init(self): def test_init(self):
""" Initialising the app with the extension. """ """ Initialising the app with the extension. """
assert hasattr(self.app, "senpy") assert hasattr(self.app, "senpy")
tapp = Flask("temp app") tapp = Flask("temp app")
tsen = Senpy(tapp) self.senpy.init_app(tapp)
assert hasattr(tapp, "senpy") assert hasattr(tapp, "senpy")
def test_discovery(self): def test_discovery(self):
""" Discovery of plugins in given folders. """ """ Discovery of plugins in given folders. """
# noinspection PyProtectedMember
assert self.dir in self.senpy._search_folders assert self.dir in self.senpy._search_folders
print self.senpy.plugins print self.senpy.plugins
assert "dummy" in self.senpy.plugins assert "Dummy" in self.senpy.plugins
def test_enabling(self): def test_enabling(self):
""" Enabling a plugin """ """ Enabling a plugin """
self.senpy.enable_plugin("dummy") self.senpy.activate_all(sync=True)
assert self.senpy.plugins["dummy"].enabled assert len(self.senpy.plugins) == 2
assert self.senpy.plugins["Sleep"].is_activated
def test_disabling(self): def test_disabling(self):
""" Disabling a plugin """ """ Disabling a plugin """
self.senpy.enable_plugin("dummy") self.senpy.deactivate_all(sync=True)
self.senpy.disable_plugin("dummy") assert self.senpy.plugins["Dummy"].is_activated == False
assert self.senpy.plugins["dummy"].enabled == False assert self.senpy.plugins["Sleep"].is_activated == False
def test_default(self): def test_default(self):
""" Default plugin should be set """ """ Default plugin should be set """
assert self.senpy.default_plugin assert self.senpy.default_plugin
assert self.senpy.default_plugin == "dummy" assert self.senpy.default_plugin == "Dummy"
def test_analyse(self): def test_analyse(self):
""" Using a plugin """ """ Using a plugin """
with mock.patch.object(self.senpy.plugins["dummy"], "analyse") as mocked: # I was using mock until plugin started inheriting Leaf (defaultdict with
self.senpy.analyse(algorithm="dummy", input="tupni", output="tuptuo") # __setattr__ and __getattr__.
self.senpy.analyse(input="tupni", output="tuptuo") r1 = self.senpy.analyse(algorithm="Dummy", input="tupni", output="tuptuo")
mocked.assert_any_call(input="tupni", output="tuptuo", algorithm="dummy") r2 = self.senpy.analyse(input="tupni", output="tuptuo")
mocked.assert_any_call(input="tupni", output="tuptuo") assert r1.analysis[0].id[:5] == "Dummy"
assert r2.analysis[0].id[:5] == "Dummy"
for plug in self.senpy.plugins: for plug in self.senpy.plugins:
self.senpy.disable_plugin(plug) self.senpy.deactivate_plugin(plug, sync=True)
resp = self.senpy.analyse(input="tupni") resp = self.senpy.analyse(input="tupni")
logging.debug("Response: {}".format(resp)) logging.debug("Response: {}".format(resp))
assert resp["status"] == 400 assert resp["status"] == 400
def test_filtering(self): def test_filtering(self):
""" Filtering plugins """ """ Filtering plugins """
assert len(self.senpy.filter_plugins(name="dummy"))>0 assert len(self.senpy.filter_plugins(name="Dummy")) > 0
assert not len(self.senpy.filter_plugins(name="notdummy")) assert not len(self.senpy.filter_plugins(name="notdummy"))
assert self.senpy.filter_plugins(name="dummy", enabled=True) assert self.senpy.filter_plugins(name="Dummy", is_activated=True)
self.senpy.disable_plugin("dummy") self.senpy.deactivate_plugin("Dummy", sync=True)
assert not len(self.senpy.filter_plugins(name="dummy", enabled=True)) assert not len(self.senpy.filter_plugins(name="Dummy", is_activated=True))

View File

@@ -0,0 +1,36 @@
import os
import logging
try:
import unittest.mock as mock
except ImportError:
import mock
import json
import os
from unittest import TestCase
from senpy.models import Response
from senpy.plugins import SenpyPlugin
class ModelsTest(TestCase):
def test_response(self):
r = Response(context=os.path.normpath(os.path.join(__file__, "..", "..", "context.jsonld")))
assert("@context" in r)
assert("marl" in r.context)
r2 = Response(context=json.loads('{"test": "roger"}'))
assert("test" in r2.context)
r3 = Response(context=None)
del r3.context
assert("@context" not in r3)
assert("entries" in r3)
assert("analysis" in r3)
def test_opinions(self):
pass
def test_frame_plugin(self):
p = SenpyPlugin({"name": "dummy", "version": 0})
c = p.frame()
assert "info" not in c
def test_frame_response(self):
pass

View File

@@ -0,0 +1,14 @@
from senpy.plugins import SenpyPlugin
from senpy.models import Response
from time import sleep
class SleepPlugin(SenpyPlugin):
def __init__(self, info, *args, **kwargs):
super(SleepPlugin, self).__init__(info, *args, **kwargs)
self.timeout = int(info["timeout"])
def activate(self, *args, **kwargs):
sleep(self.timeout)
def analyse(self, *args, **kwargs):
return Response()

View File

@@ -0,0 +1,8 @@
{
"name": "Sleep",
"module": "sleep",
"description": "I am dummy",
"author": "@balkian",
"version": "0.1",
"timeout": "2"
}